Add internal hidden rules for testing (#9747)

Updated implementation of https://github.com/astral-sh/ruff/pull/7369
which was left out in the cold.

This was motivated again following changes in #9691 and #9689 where we
could not test the changes without actually deprecating or removing
rules.

---

Follow-up to discussion in https://github.com/astral-sh/ruff/pull/7210

Moves integration tests from using rules that are transitively in
nursery / preview groups to dedicated test rules that only exist during
development. These rules always raise violations (they do not require
specific file behavior). The rules are not available in production or in
the documentation.

Uses features instead of `cfg(test)` for cross-crate support per
https://github.com/rust-lang/cargo/issues/8379
This commit is contained in:
Zanie Blue 2024-02-01 08:44:51 -06:00 committed by GitHub
parent 2cc8acb0b7
commit f18e7d40ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 541 additions and 196 deletions

View file

@ -117,7 +117,10 @@ jobs:
tool: cargo-insta tool: cargo-insta
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: "Run tests" - name: "Run tests"
run: cargo insta test --all --all-features --unreferenced reject run: cargo insta test --all --exclude ruff_dev --all-features --unreferenced reject
- name: "Run dev tests"
# e.g. generating the schema — these should not run with all features enabled
run: cargo insta test -p ruff_dev --unreferenced reject
# Check for broken links in the documentation. # Check for broken links in the documentation.
- run: cargo doc --all --no-deps - run: cargo doc --all --no-deps
env: env:
@ -146,7 +149,7 @@ jobs:
- name: "Run tests" - name: "Run tests"
shell: bash shell: bash
# We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows # We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows
run: cargo insta test --all --all-features run: cargo insta test --all --exclude ruff_dev --all-features
cargo-test-wasm: cargo-test-wasm:
name: "cargo test (wasm)" name: "cargo test (wasm)"

View file

@ -54,6 +54,8 @@ walkdir = { workspace = true }
wild = { workspace = true } wild = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Enable test rules during development
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
assert_cmd = { workspace = true } assert_cmd = { workspace = true }
# Avoid writing colored snapshots when running tests from the terminal # Avoid writing colored snapshots when running tests from the terminal
colored = { workspace = true, features = ["no-color"]} colored = { workspace = true, features = ["no-color"]}

View file

@ -799,15 +799,18 @@ fn show_statistics() {
#[test] #[test]
fn nursery_prefix() { fn nursery_prefix() {
// `--select E` should detect E741, but not E225, which is in the nursery. // Should only detect RUF90X, but not the unstable test rules
let mut cmd = RuffCheck::default().args(["--select", "E"]).build(); let mut cmd = RuffCheck::default().args(["--select", "RUF9"]).build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:1: E741 Ambiguous variable name: `I` -:1:1: RUF900 Hey this is a stable test rule.
Found 1 error. -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
Found 4 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr ----- ----- stderr -----
"###); "###);
@ -815,16 +818,19 @@ fn nursery_prefix() {
#[test] #[test]
fn nursery_all() { fn nursery_all() {
// `--select ALL` should detect E741, but not E225, which is in the nursery. // Should detect RUF90X, but not the unstable test rules
let mut cmd = RuffCheck::default().args(["--select", "ALL"]).build(); let mut cmd = RuffCheck::default().args(["--select", "ALL"]).build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:1: E741 Ambiguous variable name: `I`
-:1:1: D100 Missing docstring in public module -:1:1: D100 Missing docstring in public module
Found 2 errors. -:1:1: RUF900 Hey this is a stable test rule.
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
Found 5 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr ----- ----- stderr -----
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`. warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
@ -834,35 +840,32 @@ fn nursery_all() {
#[test] #[test]
fn nursery_direct() { fn nursery_direct() {
// `--select E225` should detect E225. // Should warn that the nursery rule is selected without preview flag but still
let mut cmd = RuffCheck::default().args(["--select", "E225"]).build(); // include the diagnostic
assert_cmd_snapshot!(cmd let mut cmd = RuffCheck::default().args(["--select", "RUF912"]).build();
.pass_stdin("I=42\n"), @r###" assert_cmd_snapshot!(cmd, @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:2: E225 [*] Missing whitespace around operator -:1:1: RUF912 Hey this is a nursery test rule.
Found 1 error. Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr ----- ----- stderr -----
warning: Selection of nursery rule `E225` without the `--preview` flag is deprecated. warning: Selection of nursery rule `RUF912` without the `--preview` flag is deprecated.
"###); "###);
} }
#[test] #[test]
fn nursery_group_selector() { fn nursery_group_selector() {
// Only nursery rules should be detected e.g. E225 and a warning should be displayed // Only nursery rules should be detected e.g. RUF912
let mut cmd = RuffCheck::default().args(["--select", "NURSERY"]).build(); let mut cmd = RuffCheck::default().args(["--select", "NURSERY"]).build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:1: CPY001 Missing copyright notice at top of file -:1:1: CPY001 Missing copyright notice at top of file
-:1:2: E225 [*] Missing whitespace around operator -:1:1: RUF912 Hey this is a nursery test rule.
Found 2 errors. Found 2 errors.
[*] 1 fixable with the `--fix` option.
----- stderr ----- ----- stderr -----
warning: The `NURSERY` selector has been deprecated. Use the `--preview` flag instead. warning: The `NURSERY` selector has been deprecated. Use the `--preview` flag instead.
@ -871,12 +874,11 @@ fn nursery_group_selector() {
#[test] #[test]
fn nursery_group_selector_preview_enabled() { fn nursery_group_selector_preview_enabled() {
// Only nursery rules should be detected e.g. E225 and a warning should be displayed // A warning should be displayed due to deprecated selector usage
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "NURSERY", "--preview"]) .args(["--select", "NURSERY", "--preview"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###"
success: false success: false
exit_code: 2 exit_code: 2
----- stdout ----- ----- stdout -----
@ -889,19 +891,22 @@ fn nursery_group_selector_preview_enabled() {
#[test] #[test]
fn preview_enabled_prefix() { fn preview_enabled_prefix() {
// E741 and E225 (preview) should both be detected // All the RUF9XX test rules should be triggered
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "E", "--preview"]) .args(["--select", "RUF9", "--preview"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:1: E741 Ambiguous variable name: `I` -:1:1: RUF900 Hey this is a stable test rule.
-:1:2: E225 [*] Missing whitespace around operator -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
Found 2 errors. -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
[*] 1 fixable with the `--fix` option. -:1:1: RUF903 Hey this is a stable test rule with a display only fix.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
Found 6 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr ----- ----- stderr -----
"###); "###);
@ -912,17 +917,20 @@ fn preview_enabled_all() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "ALL", "--preview"]) .args(["--select", "ALL", "--preview"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:1: E741 Ambiguous variable name: `I`
-:1:1: D100 Missing docstring in public module -:1:1: D100 Missing docstring in public module
-:1:1: CPY001 Missing copyright notice at top of file -:1:1: CPY001 Missing copyright notice at top of file
-:1:2: E225 [*] Missing whitespace around operator -:1:1: RUF900 Hey this is a stable test rule.
Found 4 errors. -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
[*] 1 fixable with the `--fix` option. -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
Found 8 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr ----- ----- stderr -----
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`. warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
@ -932,18 +940,16 @@ fn preview_enabled_all() {
#[test] #[test]
fn preview_enabled_direct() { fn preview_enabled_direct() {
// E225 should be detected without warning // Should be enabled without warning
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "E225", "--preview"]) .args(["--select", "RUF911", "--preview"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:2: E225 [*] Missing whitespace around operator -:1:1: RUF911 Hey this is a preview test rule.
Found 1 error. Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr ----- ----- stderr -----
"###); "###);
@ -951,45 +957,40 @@ fn preview_enabled_direct() {
#[test] #[test]
fn preview_disabled_direct() { fn preview_disabled_direct() {
// FURB145 is preview not nursery so selecting should be empty // RUFF911 is preview not nursery so the selection should be empty
let mut cmd = RuffCheck::default().args(["--select", "FURB145"]).build(); let mut cmd = RuffCheck::default().args(["--select", "RUF911"]).build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("a = l[:]\n"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
warning: Selection `FURB145` has no effect because the `--preview` flag was not included. warning: Selection `RUF911` has no effect because the `--preview` flag was not included.
"###); "###);
} }
#[test] #[test]
fn preview_disabled_prefix_empty() { fn preview_disabled_prefix_empty() {
// Warns that the selection is empty since all of the CPY rules are in preview // Warns that the selection is empty since all of the RUF91 rules are in preview
let mut cmd = RuffCheck::default().args(["--select", "CPY"]).build(); let mut cmd = RuffCheck::default().args(["--select", "RUF91"]).build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
warning: Selection `CPY` has no effect because the `--preview` flag was not included. warning: Selection `RUF91` has no effect because the `--preview` flag was not included.
"###); "###);
} }
#[test] #[test]
fn preview_disabled_does_not_warn_for_empty_ignore_selections() { fn preview_disabled_does_not_warn_for_empty_ignore_selections() {
// Does not warn that the selection is empty since the user is not trying to enable the rule // Does not warn that the selection is empty since the user is not trying to enable the rule
let mut cmd = RuffCheck::default().args(["--ignore", "CPY"]).build(); let mut cmd = RuffCheck::default().args(["--ignore", "RUF9"]).build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###" success: true
success: false exit_code: 0
exit_code: 1
----- stdout ----- ----- stdout -----
-:1:1: E741 Ambiguous variable name: `I`
Found 1 error.
----- stderr ----- ----- stderr -----
"###); "###);
@ -998,14 +999,11 @@ fn preview_disabled_does_not_warn_for_empty_ignore_selections() {
#[test] #[test]
fn preview_disabled_does_not_warn_for_empty_fixable_selections() { fn preview_disabled_does_not_warn_for_empty_fixable_selections() {
// Does not warn that the selection is empty since the user is not trying to enable the rule // Does not warn that the selection is empty since the user is not trying to enable the rule
let mut cmd = RuffCheck::default().args(["--fixable", "CPY"]).build(); let mut cmd = RuffCheck::default().args(["--fixable", "RUF9"]).build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd, @r###"
.pass_stdin("I=42\n"), @r###" success: true
success: false exit_code: 0
exit_code: 1
----- stdout ----- ----- stdout -----
-:1:1: E741 Ambiguous variable name: `I`
Found 1 error.
----- stderr ----- ----- stderr -----
"###); "###);
@ -1032,20 +1030,25 @@ fn preview_group_selector() {
#[test] #[test]
fn preview_enabled_group_ignore() { fn preview_enabled_group_ignore() {
// `--select E --ignore PREVIEW` should detect E741 and E225, which is in preview but "E" is more specific. // Should detect stable and unstable rules, RUF9 is more specific than RUF so ignore has no effect
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "E", "--ignore", "PREVIEW", "--preview"]) .args(["--select", "RUF9", "--ignore", "RUF", "--preview"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd
.pass_stdin("I=42\n"), @r###" .pass_stdin("I=42\n"), @r###"
success: false success: false
exit_code: 2 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:1: RUF900 Hey this is a stable test rule.
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
Found 6 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr ----- ----- stderr -----
error: invalid value 'PREVIEW' for '--ignore <RULE_CODE>'
For more information, try '--help'.
"###); "###);
} }
@ -1150,16 +1153,15 @@ fn check_input_from_argfile() -> Result<()> {
#[test] #[test]
fn check_hints_hidden_unsafe_fixes() { fn check_hints_hidden_unsafe_fixes() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034"]) .args(["--select", "RUF901,RUF902"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:2:7: UP034 [*] Avoid extraneous parentheses -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
Found 2 errors. Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option). [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@ -1169,14 +1171,14 @@ fn check_hints_hidden_unsafe_fixes() {
#[test] #[test]
fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() { fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
let mut cmd = RuffCheck::default().args(["--select", "F601"]).build(); let mut cmd = RuffCheck::default().args(["--select", "RUF902"]).build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd
.pass_stdin("x = {'a': 1, 'a': 1}\n"), .pass_stdin("x = {'a': 1, 'a': 1}\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
Found 1 error. Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@ -1187,16 +1189,15 @@ fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
#[test] #[test]
fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() { fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--no-unsafe-fixes"]) .args(["--select", "RUF901,RUF902", "--no-unsafe-fixes"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:2:7: UP034 [*] Avoid extraneous parentheses -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
Found 2 errors. Found 2 errors.
[*] 1 fixable with the --fix option. [*] 1 fixable with the --fix option.
@ -1207,7 +1208,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
#[test] #[test]
fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() { fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601", "--no-unsafe-fixes"]) .args(["--select", "RUF902", "--no-unsafe-fixes"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd
.pass_stdin("x = {'a': 1, 'a': 1}\n"), .pass_stdin("x = {'a': 1, 'a': 1}\n"),
@ -1215,7 +1216,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
Found 1 error. Found 1 error.
----- stderr ----- ----- stderr -----
@ -1225,16 +1226,15 @@ fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
#[test] #[test]
fn check_shows_unsafe_fixes_with_opt_in() { fn check_shows_unsafe_fixes_with_opt_in() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--unsafe-fixes"]) .args(["--select", "RUF901,RUF902", "--unsafe-fixes"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 [*] Dictionary key literal `'a'` repeated -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:2:7: UP034 [*] Avoid extraneous parentheses -:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
Found 2 errors. Found 2 errors.
[*] 2 fixable with the --fix option. [*] 2 fixable with the --fix option.
@ -1245,19 +1245,17 @@ fn check_shows_unsafe_fixes_with_opt_in() {
#[test] #[test]
fn fix_applies_safe_fixes_by_default() { fn fix_applies_safe_fixes_by_default() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--fix"]) .args(["--select", "RUF901,RUF902", "--fix"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
x = {'a': 1, 'a': 1} # fix from stable-test-rule-safe-fix
print('foo')
----- stderr ----- ----- stderr -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
Found 2 errors (1 fixed, 1 remaining). Found 2 errors (1 fixed, 1 remaining).
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
"###); "###);
@ -1266,16 +1264,15 @@ fn fix_applies_safe_fixes_by_default() {
#[test] #[test]
fn fix_applies_unsafe_fixes_with_opt_in() { fn fix_applies_unsafe_fixes_with_opt_in() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--fix", "--unsafe-fixes"]) .args(["--select", "RUF901,RUF902", "--fix", "--unsafe-fixes"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
x = {'a': 1} # fix from stable-test-rule-unsafe-fix
print('foo') # fix from stable-test-rule-safe-fix
----- stderr ----- ----- stderr -----
Found 2 errors (2 fixed, 0 remaining). Found 2 errors (2 fixed, 0 remaining).
@ -1285,7 +1282,7 @@ fn fix_applies_unsafe_fixes_with_opt_in() {
#[test] #[test]
fn fix_does_not_apply_display_only_fixes() { fn fix_does_not_apply_display_only_fixes() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "B006", "--fix"]) .args(["--select", "RUF903", "--fix"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd
.pass_stdin("def add_to_list(item, some_list=[]): ..."), .pass_stdin("def add_to_list(item, some_list=[]): ..."),
@ -1295,7 +1292,7 @@ fn fix_does_not_apply_display_only_fixes() {
----- stdout ----- ----- stdout -----
def add_to_list(item, some_list=[]): ... def add_to_list(item, some_list=[]): ...
----- stderr ----- ----- stderr -----
-:1:33: B006 Do not use mutable data structures for argument defaults -:1:1: RUF903 Hey this is a stable test rule with a display only fix.
Found 1 error. Found 1 error.
"###); "###);
} }
@ -1303,7 +1300,7 @@ fn fix_does_not_apply_display_only_fixes() {
#[test] #[test]
fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() { fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "B006", "--fix", "--unsafe-fixes"]) .args(["--select", "RUF903", "--fix", "--unsafe-fixes"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd
.pass_stdin("def add_to_list(item, some_list=[]): ..."), .pass_stdin("def add_to_list(item, some_list=[]): ..."),
@ -1313,7 +1310,7 @@ fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
----- stdout ----- ----- stdout -----
def add_to_list(item, some_list=[]): ... def add_to_list(item, some_list=[]): ...
----- stderr ----- ----- stderr -----
-:1:33: B006 Do not use mutable data structures for argument defaults -:1:1: RUF903 Hey this is a stable test rule with a display only fix.
Found 1 error. Found 1 error.
"###); "###);
} }
@ -1321,19 +1318,16 @@ fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
#[test] #[test]
fn fix_only_unsafe_fixes_available() { fn fix_only_unsafe_fixes_available() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601", "--fix"]) .args(["--select", "RUF902", "--fix"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
x = {'a': 1, 'a': 1}
print(('foo'))
----- stderr ----- ----- stderr -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
Found 1 error. Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
"###); "###);
@ -1342,16 +1336,14 @@ fn fix_only_unsafe_fixes_available() {
#[test] #[test]
fn fix_only_flag_applies_safe_fixes_by_default() { fn fix_only_flag_applies_safe_fixes_by_default() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--fix-only"]) .args(["--select", "RUF901,RUF902", "--fix-only"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
x = {'a': 1, 'a': 1} # fix from stable-test-rule-safe-fix
print('foo')
----- stderr ----- ----- stderr -----
Fixed 1 error (1 additional fix available with `--unsafe-fixes`). Fixed 1 error (1 additional fix available with `--unsafe-fixes`).
@ -1361,16 +1353,15 @@ fn fix_only_flag_applies_safe_fixes_by_default() {
#[test] #[test]
fn fix_only_flag_applies_unsafe_fixes_with_opt_in() { fn fix_only_flag_applies_unsafe_fixes_with_opt_in() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--fix-only", "--unsafe-fixes"]) .args(["--select", "RUF901,RUF902", "--fix-only", "--unsafe-fixes"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
x = {'a': 1} # fix from stable-test-rule-unsafe-fix
print('foo') # fix from stable-test-rule-safe-fix
----- stderr ----- ----- stderr -----
Fixed 2 errors. Fixed 2 errors.
@ -1380,18 +1371,15 @@ fn fix_only_flag_applies_unsafe_fixes_with_opt_in() {
#[test] #[test]
fn diff_shows_safe_fixes_by_default() { fn diff_shows_safe_fixes_by_default() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--diff"]) .args(["--select", "RUF901,RUF902", "--diff"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
@@ -1,2 +1,2 @@ @@ -0,0 +1 @@
x = {'a': 1, 'a': 1} +# fix from stable-test-rule-safe-fix
-print(('foo'))
+print('foo')
----- stderr ----- ----- stderr -----
@ -1403,19 +1391,16 @@ fn diff_shows_safe_fixes_by_default() {
#[test] #[test]
fn diff_shows_unsafe_fixes_with_opt_in() { fn diff_shows_unsafe_fixes_with_opt_in() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--diff", "--unsafe-fixes"]) .args(["--select", "RUF901,RUF902", "--diff", "--unsafe-fixes"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
@@ -1,2 +1,2 @@ @@ -0,0 +1,2 @@
-x = {'a': 1, 'a': 1} +# fix from stable-test-rule-unsafe-fix
-print(('foo')) +# fix from stable-test-rule-safe-fix
+x = {'a': 1}
+print('foo')
----- stderr ----- ----- stderr -----
@ -1427,7 +1412,7 @@ fn diff_shows_unsafe_fixes_with_opt_in() {
#[test] #[test]
fn diff_does_not_show_display_only_fixes_with_unsafe_fixes_enabled() { fn diff_does_not_show_display_only_fixes_with_unsafe_fixes_enabled() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "B006", "--diff", "--unsafe-fixes"]) .args(["--select", "RUF903", "--diff", "--unsafe-fixes"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd
.pass_stdin("def add_to_list(item, some_list=[]): ..."), .pass_stdin("def add_to_list(item, some_list=[]): ..."),
@ -1443,10 +1428,9 @@ fn diff_does_not_show_display_only_fixes_with_unsafe_fixes_enabled() {
#[test] #[test]
fn diff_only_unsafe_fixes_available() { fn diff_only_unsafe_fixes_available() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.args(["--select", "F601", "--diff"]) .args(["--select", "RUF902", "--diff"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: true success: true
exit_code: 0 exit_code: 0
@ -1466,22 +1450,21 @@ fn check_extend_unsafe_fixes() -> Result<()> {
&ruff_toml, &ruff_toml,
r#" r#"
[lint] [lint]
extend-unsafe-fixes = ["UP034"] extend-unsafe-fixes = ["RUF901"]
"#, "#,
)?; )?;
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.config(&ruff_toml) .config(&ruff_toml)
.args(["--select", "F601,UP034"]) .args(["--select", "RUF901,RUF902"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF901 Hey this is a stable test rule with a safe fix.
-:2:7: UP034 Avoid extraneous parentheses -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
Found 2 errors. Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option). No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
@ -1499,22 +1482,21 @@ fn check_extend_safe_fixes() -> Result<()> {
&ruff_toml, &ruff_toml,
r#" r#"
[lint] [lint]
extend-safe-fixes = ["F601"] extend-safe-fixes = ["RUF902"]
"#, "#,
)?; )?;
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.config(&ruff_toml) .config(&ruff_toml)
.args(["--select", "F601,UP034"]) .args(["--select", "RUF901,RUF902"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 [*] Dictionary key literal `'a'` repeated -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:2:7: UP034 [*] Avoid extraneous parentheses -:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
Found 2 errors. Found 2 errors.
[*] 2 fixable with the `--fix` option. [*] 2 fixable with the `--fix` option.
@ -1533,25 +1515,24 @@ fn check_extend_unsafe_fixes_conflict_with_extend_safe_fixes() -> Result<()> {
&ruff_toml, &ruff_toml,
r#" r#"
[lint] [lint]
extend-unsafe-fixes = ["UP034"] extend-unsafe-fixes = ["RUF902"]
extend-safe-fixes = ["UP034"] extend-safe-fixes = ["RUF902"]
"#, "#,
)?; )?;
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.config(&ruff_toml) .config(&ruff_toml)
.args(["--select", "F601,UP034"]) .args(["--select", "RUF901,RUF902"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd,
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###" @r###"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:2:7: UP034 Avoid extraneous parentheses -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
Found 2 errors. Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option). [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr ----- ----- stderr -----
"###); "###);
@ -1569,14 +1550,14 @@ fn check_extend_unsafe_fixes_conflict_with_extend_safe_fixes_by_specificity() ->
r#" r#"
target-version = "py310" target-version = "py310"
[lint] [lint]
extend-unsafe-fixes = ["UP", "UP034"] extend-unsafe-fixes = ["RUF", "RUF901"]
extend-safe-fixes = ["UP03"] extend-safe-fixes = ["RUF9"]
"#, "#,
)?; )?;
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
.config(&ruff_toml) .config(&ruff_toml)
.args(["--select", "F601,UP018,UP034,UP038"]) .args(["--select", "RUF9"])
.build(); .build();
assert_cmd_snapshot!(cmd assert_cmd_snapshot!(cmd
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\nprint(str('foo'))\nisinstance(x, (int, str))\n"), .pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\nprint(str('foo'))\nisinstance(x, (int, str))\n"),
@ -1584,12 +1565,12 @@ extend-safe-fixes = ["UP03"]
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated -:1:1: RUF900 Hey this is a stable test rule.
-:2:7: UP034 Avoid extraneous parentheses -:1:1: RUF901 Hey this is a stable test rule with a safe fix.
-:3:7: UP018 Unnecessary `str` call (rewrite as a literal) -:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
-:4:1: UP038 [*] Use `X | Y` in `isinstance` call instead of `(X, Y)` -:1:1: RUF903 Hey this is a stable test rule with a display only fix.
Found 4 errors. Found 4 errors.
[*] 1 fixable with the `--fix` option (3 hidden fixes can be enabled with the `--unsafe-fixes` option). [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr ----- ----- stderr -----
"###); "###);

View file

@ -85,6 +85,8 @@ tempfile = { workspace = true }
[features] [features]
default = [] default = []
schemars = ["dep:schemars"] schemars = ["dep:schemars"]
# Enables rules for internal integration tests
test-rules = []
[lints] [lints]
workspace = true workspace = true

View file

@ -934,6 +934,20 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg), (Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml), (Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
#[cfg(feature = "test-rules")]
(Ruff, "900") => (RuleGroup::Stable, rules::ruff::rules::StableTestRule),
#[cfg(feature = "test-rules")]
(Ruff, "901") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleSafeFix),
#[cfg(feature = "test-rules")]
(Ruff, "902") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleUnsafeFix),
#[cfg(feature = "test-rules")]
(Ruff, "903") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleDisplayOnlyFix),
#[cfg(feature = "test-rules")]
(Ruff, "911") => (RuleGroup::Preview, rules::ruff::rules::PreviewTestRule),
#[cfg(feature = "test-rules")]
#[allow(deprecated)]
(Ruff, "912") => (RuleGroup::Nursery, rules::ruff::rules::NurseryTestRule),
// flake8-django // flake8-django
(Flake8Django, "001") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoNullableModelStringField), (Flake8Django, "001") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoNullableModelStringField),

View file

@ -33,6 +33,8 @@ use crate::message::Message;
use crate::noqa::add_noqa; use crate::noqa::add_noqa;
use crate::registry::{AsRule, Rule, RuleSet}; use crate::registry::{AsRule, Rule, RuleSet};
use crate::rules::pycodestyle; use crate::rules::pycodestyle;
#[cfg(feature = "test-rules")]
use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES};
use crate::settings::types::UnsafeFixes; use crate::settings::types::UnsafeFixes;
use crate::settings::{flags, LinterSettings}; use crate::settings::{flags, LinterSettings};
use crate::source_kind::SourceKind; use crate::source_kind::SourceKind;
@ -214,6 +216,34 @@ pub fn check_path(
)); ));
} }
// Raise violations for internal test rules
#[cfg(feature = "test-rules")]
{
for test_rule in TEST_RULES {
if !settings.rules.enabled(*test_rule) {
continue;
}
let diagnostic = match test_rule {
Rule::StableTestRule => test_rules::StableTestRule::diagnostic(locator, indexer),
Rule::StableTestRuleSafeFix => {
test_rules::StableTestRuleSafeFix::diagnostic(locator, indexer)
}
Rule::StableTestRuleUnsafeFix => {
test_rules::StableTestRuleUnsafeFix::diagnostic(locator, indexer)
}
Rule::StableTestRuleDisplayOnlyFix => {
test_rules::StableTestRuleDisplayOnlyFix::diagnostic(locator, indexer)
}
Rule::NurseryTestRule => test_rules::NurseryTestRule::diagnostic(locator, indexer),
Rule::PreviewTestRule => test_rules::PreviewTestRule::diagnostic(locator, indexer),
_ => unreachable!("All test rules must have an implementation"),
};
if let Some(diagnostic) = diagnostic {
diagnostics.push(diagnostic);
}
}
}
// Ignore diagnostics based on per-file-ignores. // Ignore diagnostics based on per-file-ignores.
let per_file_ignores = if !diagnostics.is_empty() && !settings.per_file_ignores.is_empty() { let per_file_ignores = if !diagnostics.is_empty() && !settings.per_file_ignores.is_empty() {
fs::ignores_from_path(path, &settings.per_file_ignores) fs::ignores_from_path(path, &settings.per_file_ignores)
@ -539,7 +569,7 @@ pub fn lint_fix<'a>(
// Increment the iteration count. // Increment the iteration count.
iterations += 1; iterations += 1;
// Re-run the linter pass (by avoiding the break). // Re-run the linter pass (by avoiding the return).
continue; continue;
} }

View file

@ -18,6 +18,8 @@ pub(crate) use quadratic_list_summation::*;
pub(crate) use sort_dunder_all::*; pub(crate) use sort_dunder_all::*;
pub(crate) use sort_dunder_slots::*; pub(crate) use sort_dunder_slots::*;
pub(crate) use static_key_dict_comprehension::*; pub(crate) use static_key_dict_comprehension::*;
#[cfg(feature = "test-rules")]
pub(crate) use test_rules::*;
pub(crate) use unnecessary_dict_comprehension_for_iterable::*; pub(crate) use unnecessary_dict_comprehension_for_iterable::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*; pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*; pub(crate) use unnecessary_key_check::*;
@ -45,6 +47,8 @@ mod sequence_sorting;
mod sort_dunder_all; mod sort_dunder_all;
mod sort_dunder_slots; mod sort_dunder_slots;
mod static_key_dict_comprehension; mod static_key_dict_comprehension;
#[cfg(feature = "test-rules")]
pub(crate) mod test_rules;
mod unnecessary_dict_comprehension_for_iterable; mod unnecessary_dict_comprehension_for_iterable;
mod unnecessary_iterable_allocation_for_first_element; mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check; mod unnecessary_key_check;

View file

@ -0,0 +1,291 @@
/// Fake rules for testing Ruff's behavior
///
/// All of these rules should be assigned to the RUF9XX codes.
///
/// Implementing a new test rule involves:
///
/// - Writing an empty struct for the rule
/// - Adding to the rule registry
/// - Adding to the `TEST_RULES` constant
/// - Implementing `Violation` for the rule
/// - Implementing `TestRule` for the rule
/// - Adding a match arm in `linter::check_path`
///
/// Rules that provide a fix _must_ not raise unconditionally or the linter
/// will not converge.
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use ruff_text_size::TextSize;
use crate::registry::Rule;
/// Check if a comment exists anywhere in a the given file
fn comment_exists(text: &str, locator: &Locator, indexer: &Indexer) -> bool {
for range in indexer.comment_ranges() {
let comment_text = locator.slice(range);
if text.trim_end() == comment_text {
return true;
}
}
false
}
pub(crate) const TEST_RULES: &[Rule] = &[
Rule::StableTestRule,
Rule::StableTestRuleSafeFix,
Rule::StableTestRuleUnsafeFix,
Rule::StableTestRuleDisplayOnlyFix,
Rule::PreviewTestRule,
Rule::NurseryTestRule,
];
pub(crate) trait TestRule {
fn diagnostic(locator: &Locator, indexer: &Indexer) -> Option<Diagnostic>;
}
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct StableTestRule;
impl Violation for StableTestRule {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a stable test rule.")
}
}
impl TestRule for StableTestRule {
fn diagnostic(_locator: &Locator, _indexer: &Indexer) -> Option<Diagnostic> {
Some(Diagnostic::new(
StableTestRule,
ruff_text_size::TextRange::default(),
))
}
}
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct StableTestRuleSafeFix;
impl Violation for StableTestRuleSafeFix {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a stable test rule with a safe fix.")
}
}
impl TestRule for StableTestRuleSafeFix {
fn diagnostic(locator: &Locator, indexer: &Indexer) -> Option<Diagnostic> {
let comment = format!("# fix from stable-test-rule-safe-fix\n");
if comment_exists(&comment, locator, indexer) {
None
} else {
Some(
Diagnostic::new(StableTestRuleSafeFix, ruff_text_size::TextRange::default())
.with_fix(Fix::safe_edit(Edit::insertion(
comment.to_string(),
TextSize::new(0),
))),
)
}
}
}
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct StableTestRuleUnsafeFix;
impl Violation for StableTestRuleUnsafeFix {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a stable test rule with an unsafe fix.")
}
}
impl TestRule for StableTestRuleUnsafeFix {
fn diagnostic(locator: &Locator, indexer: &Indexer) -> Option<Diagnostic> {
let comment = format!("# fix from stable-test-rule-unsafe-fix\n");
if comment_exists(&comment, locator, indexer) {
None
} else {
Some(
Diagnostic::new(
StableTestRuleUnsafeFix,
ruff_text_size::TextRange::default(),
)
.with_fix(Fix::unsafe_edit(Edit::insertion(
comment.to_string(),
TextSize::new(0),
))),
)
}
}
}
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct StableTestRuleDisplayOnlyFix;
impl Violation for StableTestRuleDisplayOnlyFix {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a stable test rule with a display only fix.")
}
}
impl TestRule for StableTestRuleDisplayOnlyFix {
fn diagnostic(locator: &Locator, indexer: &Indexer) -> Option<Diagnostic> {
let comment = format!("# fix from stable-test-rule-display-only-fix\n");
if comment_exists(&comment, locator, indexer) {
None
} else {
Some(
Diagnostic::new(
StableTestRuleDisplayOnlyFix,
ruff_text_size::TextRange::default(),
)
.with_fix(Fix::display_only_edit(Edit::insertion(
comment.to_string(),
TextSize::new(0),
))),
)
}
}
}
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct PreviewTestRule;
impl Violation for PreviewTestRule {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a preview test rule.")
}
}
impl TestRule for PreviewTestRule {
fn diagnostic(_locator: &Locator, _indexer: &Indexer) -> Option<Diagnostic> {
Some(Diagnostic::new(
PreviewTestRule,
ruff_text_size::TextRange::default(),
))
}
}
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct NurseryTestRule;
impl Violation for NurseryTestRule {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a nursery test rule.")
}
}
impl TestRule for NurseryTestRule {
fn diagnostic(_locator: &Locator, _indexer: &Indexer) -> Option<Diagnostic> {
Some(Diagnostic::new(
NurseryTestRule,
ruff_text_size::TextRange::default(),
))
}
}

View file

@ -8,7 +8,7 @@ use syn::{
Ident, ItemFn, LitStr, Pat, Path, Stmt, Token, Ident, ItemFn, LitStr, Pat, Path, Stmt, Token,
}; };
use crate::rule_code_prefix::{get_prefix_ident, if_all_same}; use crate::rule_code_prefix::{get_prefix_ident, intersection_all};
/// A rule entry in the big match statement such a /// A rule entry in the big match statement such a
/// `(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),` /// `(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),`
@ -142,12 +142,13 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
for (prefix, rules) in &rules_by_prefix { for (prefix, rules) in &rules_by_prefix {
let prefix_ident = get_prefix_ident(prefix); let prefix_ident = get_prefix_ident(prefix);
let attr = match if_all_same(rules.iter().map(|(.., attrs)| attrs)) { let attrs = intersection_all(rules.iter().map(|(.., attrs)| attrs.as_slice()));
Some(attr) => quote!(#(#attr)*), let attrs = match attrs.as_slice() {
None => quote!(), [] => quote!(),
[..] => quote!(#(#attrs)*),
}; };
all_codes.push(quote! { all_codes.push(quote! {
#attr Self::#linter(#linter::#prefix_ident) #attrs Self::#linter(#linter::#prefix_ident)
}); });
} }
@ -159,12 +160,13 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
quote!(#(#attrs)* Rule::#rule_name) quote!(#(#attrs)* Rule::#rule_name)
}); });
let prefix_ident = get_prefix_ident(&prefix); let prefix_ident = get_prefix_ident(&prefix);
let attr = match if_all_same(rules.iter().map(|(.., attrs)| attrs)) { let attrs = intersection_all(rules.iter().map(|(.., attrs)| attrs.as_slice()));
Some(attr) => quote!(#(#attr)*), let attrs = match attrs.as_slice() {
None => quote!(), [] => quote!(),
[..] => quote!(#(#attrs)*),
}; };
prefix_into_iter_match_arms.extend(quote! { prefix_into_iter_match_arms.extend(quote! {
#attr #linter::#prefix_ident => vec![#(#rule_paths,)*].into_iter(), #attrs #linter::#prefix_ident => vec![#(#rule_paths,)*].into_iter(),
}); });
} }

View file

@ -89,21 +89,30 @@ fn attributes_for_prefix(
codes: &BTreeSet<String>, codes: &BTreeSet<String>,
attributes: &BTreeMap<String, &[Attribute]>, attributes: &BTreeMap<String, &[Attribute]>,
) -> proc_macro2::TokenStream { ) -> proc_macro2::TokenStream {
match if_all_same(codes.iter().map(|code| attributes[code])) { let attrs = intersection_all(codes.iter().map(|code| attributes[code]));
Some(attr) => quote!(#(#attr)*), match attrs.as_slice() {
None => quote!(), [] => quote!(),
[..] => quote!(#(#attrs)*),
} }
} }
/// If all values in an iterator are the same, return that value. Otherwise, /// Collect all the items from an iterable of slices that are present in all slices.
/// return `None`. pub(crate) fn intersection_all<'a, T: PartialEq>(
pub(crate) fn if_all_same<T: PartialEq>(iter: impl Iterator<Item = T>) -> Option<T> { mut slices: impl Iterator<Item = &'a [T]>,
let mut iter = iter.peekable(); ) -> Vec<&'a T> {
let first = iter.next()?; if let Some(slice) = slices.next() {
if iter.all(|x| x == first) { // Collect all the items in the first slice
Some(first) let mut intersection = Vec::with_capacity(slice.len());
for item in slice {
intersection.push(item);
}
// Then only keep items that are present in each of the remaining slices
for slice in slices {
intersection.retain(|item| slice.contains(item));
}
intersection
} else { } else {
None Vec::new()
} }
} }

View file

@ -42,6 +42,8 @@ strum = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Enable test rules during development
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
tempfile = { workspace = true } tempfile = { workspace = true }
[features] [features]

View file

@ -1184,6 +1184,7 @@ mod tests {
Rule::DeleteFullSlice, Rule::DeleteFullSlice,
Rule::CheckAndRemoveFromSet, Rule::CheckAndRemoveFromSet,
Rule::QuadraticListSummation, Rule::QuadraticListSummation,
Rule::NurseryTestRule,
]; ];
const PREVIEW_RULES: &[Rule] = &[ const PREVIEW_RULES: &[Rule] = &[
@ -1201,6 +1202,7 @@ mod tests {
Rule::UndocumentedWarn, Rule::UndocumentedWarn,
Rule::UnnecessaryEnumerate, Rule::UnnecessaryEnumerate,
Rule::MathConstant, Rule::MathConstant,
Rule::PreviewTestRule,
]; ];
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]

View file

@ -206,6 +206,9 @@ class CheckOptions(CommandOptions):
"check", "check",
"--no-cache", "--no-cache",
"--exit-zero", "--exit-zero",
# Ignore internal test rules
"--ignore",
"RUF9",
f"--{'' if self.preview else 'no-'}preview", f"--{'' if self.preview else 'no-'}preview",
] ]
if self.select: if self.select: