Disable auto-fix when source has syntax errors (#12134)

## Summary

This PR updates Ruff to **not** generate auto-fixes if the source code
contains syntax errors as determined by the parser.

The main motivation behind this is to avoid infinite autofix loop when
the token-based rules are run over any source with syntax errors in
#11950.

Although even after this, it's not certain that there won't be an
infinite autofix loop because the logic might be incorrect. For example,
https://github.com/astral-sh/ruff/issues/12094 and
https://github.com/astral-sh/ruff/pull/12136.

This requires updating the test infrastructure to not validate for fix
availability status when the source contained syntax errors. This is
required because otherwise the fuzzer might fail as it uses the test
function to run the linter and validate the source code.

resolves: #11455 

## Test Plan

`cargo insta test`
This commit is contained in:
Dhruv Manilawala 2024-07-02 14:22:51 +05:30 committed by GitHub
parent dcb9523b1e
commit 88a4cc41f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 180 additions and 155 deletions

View file

@ -565,10 +565,6 @@ foo(
**kwargs **kwargs
} }
(
*args
)
{ {
*args *args
} }

View file

@ -0,0 +1,3 @@
(
*args
)

View file

@ -288,6 +288,7 @@ pub fn check_path(
} }
} }
if parsed.is_valid() {
// Remove fixes for any rules marked as unfixable. // Remove fixes for any rules marked as unfixable.
for diagnostic in &mut diagnostics { for diagnostic in &mut diagnostics {
if !settings.rules.should_fix(diagnostic.kind.rule()) { if !settings.rules.should_fix(diagnostic.kind.rule()) {
@ -306,6 +307,12 @@ pub fn check_path(
} }
} }
} }
} else {
// Avoid fixing in case the source code contains syntax errors.
for diagnostic in &mut diagnostics {
diagnostic.fix = None;
}
}
diagnostics diagnostics
} }

View file

@ -13,6 +13,7 @@ mod tests {
use crate::{assert_messages, settings}; use crate::{assert_messages, settings};
#[test_case(Path::new("COM81.py"))] #[test_case(Path::new("COM81.py"))]
#[test_case(Path::new("COM81_syntax_error.py"))]
fn rules(path: &Path) -> Result<()> { fn rules(path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy().into_owned(); let snapshot = path.to_string_lossy().into_owned();
let diagnostics = test_path( let diagnostics = test_path(

View file

@ -796,192 +796,184 @@ COM81.py:565:13: COM812 [*] Trailing comma missing
565 |+ **kwargs, 565 |+ **kwargs,
566 566 | } 566 566 | }
567 567 | 567 567 |
568 568 | ( 568 568 | {
COM81.py:569:5: SyntaxError: Starred expression cannot be used here COM81.py:569:10: COM812 [*] Trailing comma missing
| |
568 | ( 568 | {
569 | *args 569 | *args
| ^ | COM812
570 | ) 570 | }
| |
= help: Add trailing comma
Safe fix
566 566 | }
567 567 |
568 568 | {
569 |- *args
569 |+ *args,
570 570 | }
571 571 |
572 572 | [
COM81.py:573:10: COM812 [*] Trailing comma missing COM81.py:573:10: COM812 [*] Trailing comma missing
| |
572 | { 572 | [
573 | *args 573 | *args
| COM812 | COM812
574 | } 574 | ]
| |
= help: Add trailing comma = help: Add trailing comma
Safe fix Safe fix
570 570 | ) 570 570 | }
571 571 | 571 571 |
572 572 | { 572 572 | [
573 |- *args 573 |- *args
573 |+ *args, 573 |+ *args,
574 574 | } 574 574 | ]
575 575 | 575 575 |
576 576 | [ 576 576 | def foo(
COM81.py:577:10: COM812 [*] Trailing comma missing COM81.py:579:10: COM812 [*] Trailing comma missing
| |
576 | [ 577 | ham,
577 | *args 578 | spam,
579 | *args
| COM812 | COM812
578 | ] 580 | ):
581 | pass
| |
= help: Add trailing comma = help: Add trailing comma
Safe fix Safe fix
574 574 | } 576 576 | def foo(
575 575 | 577 577 | ham,
576 576 | [ 578 578 | spam,
577 |- *args 579 |- *args
577 |+ *args, 579 |+ *args,
578 578 | ] 580 580 | ):
579 579 | 581 581 | pass
580 580 | def foo( 582 582 |
COM81.py:583:10: COM812 [*] Trailing comma missing COM81.py:586:13: COM812 [*] Trailing comma missing
| |
581 | ham, 584 | ham,
582 | spam, 585 | spam,
583 | *args 586 | **kwargs
| COM812 | COM812
584 | ): 587 | ):
585 | pass 588 | pass
| |
= help: Add trailing comma = help: Add trailing comma
Safe fix Safe fix
580 580 | def foo( 583 583 | def foo(
581 581 | ham, 584 584 | ham,
582 582 | spam, 585 585 | spam,
583 |- *args 586 |- **kwargs
583 |+ *args, 586 |+ **kwargs,
584 584 | ): 587 587 | ):
585 585 | pass 588 588 | pass
586 586 | 589 589 |
COM81.py:590:13: COM812 [*] Trailing comma missing COM81.py:594:15: COM812 [*] Trailing comma missing
| |
588 | ham, 592 | spam,
589 | spam, 593 | *args,
590 | **kwargs 594 | kwarg_only
| COM812 | COM812
591 | ): 595 | ):
592 | pass 596 | pass
| |
= help: Add trailing comma = help: Add trailing comma
Safe fix Safe fix
587 587 | def foo( 591 591 | ham,
588 588 | ham, 592 592 | spam,
589 589 | spam, 593 593 | *args,
590 |- **kwargs 594 |- kwarg_only
590 |+ **kwargs, 594 |+ kwarg_only,
591 591 | ): 595 595 | ):
592 592 | pass 596 596 | pass
593 593 | 597 597 |
COM81.py:598:15: COM812 [*] Trailing comma missing COM81.py:623:20: COM812 [*] Trailing comma missing
| |
596 | spam, 621 | foo,
597 | *args, 622 | bar,
598 | kwarg_only 623 | **{'ham': spam}
| COM812 | COM812
599 | ): 624 | )
600 | pass
| |
= help: Add trailing comma = help: Add trailing comma
Safe fix Safe fix
595 595 | ham, 620 620 | result = function(
596 596 | spam, 621 621 | foo,
597 597 | *args, 622 622 | bar,
598 |- kwarg_only 623 |- **{'ham': spam}
598 |+ kwarg_only, 623 |+ **{'ham': spam},
599 599 | ): 624 624 | )
600 600 | pass 625 625 |
601 601 | 626 626 | # Make sure the COM812 and UP034 rules don't fix simultaneously and cause a syntax error.
COM81.py:627:20: COM812 [*] Trailing comma missing COM81.py:628:42: COM812 [*] Trailing comma missing
| |
625 | foo, 626 | # Make sure the COM812 and UP034 rules don't fix simultaneously and cause a syntax error.
626 | bar, 627 | the_first_one = next(
627 | **{'ham': spam} 628 | (i for i in range(10) if i // 2 == 0) # COM812 fix should include the final bracket
| COM812 | COM812
628 | ) 629 | )
| |
= help: Add trailing comma = help: Add trailing comma
Safe fix Safe fix
624 624 | result = function( 625 625 |
625 625 | foo, 626 626 | # Make sure the COM812 and UP034 rules don't fix simultaneously and cause a syntax error.
626 626 | bar, 627 627 | the_first_one = next(
627 |- **{'ham': spam} 628 |- (i for i in range(10) if i // 2 == 0) # COM812 fix should include the final bracket
627 |+ **{'ham': spam}, 628 |+ (i for i in range(10) if i // 2 == 0), # COM812 fix should include the final bracket
628 628 | ) 629 629 | )
629 629 | 630 630 |
630 630 | # Make sure the COM812 and UP034 rules don't fix simultaneously and cause a syntax error. 631 631 | foo = namedtuple(
COM81.py:632:42: COM812 [*] Trailing comma missing COM81.py:640:46: COM819 [*] Trailing comma prohibited
| |
630 | # Make sure the COM812 and UP034 rules don't fix simultaneously and cause a syntax error. 639 | # F-strings
631 | the_first_one = next( 640 | kwargs.pop("remove", f"this {trailing_comma}",)
632 | (i for i in range(10) if i // 2 == 0) # COM812 fix should include the final bracket
| COM812
633 | )
|
= help: Add trailing comma
Safe fix
629 629 |
630 630 | # Make sure the COM812 and UP034 rules don't fix simultaneously and cause a syntax error.
631 631 | the_first_one = next(
632 |- (i for i in range(10) if i // 2 == 0) # COM812 fix should include the final bracket
632 |+ (i for i in range(10) if i // 2 == 0), # COM812 fix should include the final bracket
633 633 | )
634 634 |
635 635 | foo = namedtuple(
COM81.py:644:46: COM819 [*] Trailing comma prohibited
|
643 | # F-strings
644 | kwargs.pop("remove", f"this {trailing_comma}",)
| ^ COM819 | ^ COM819
645 | 641 |
646 | raise Exception( 642 | raise Exception(
| |
= help: Remove trailing comma = help: Remove trailing comma
Safe fix Safe fix
641 641 | ) 637 637 | )
642 642 | 638 638 |
643 643 | # F-strings 639 639 | # F-strings
644 |-kwargs.pop("remove", f"this {trailing_comma}",) 640 |-kwargs.pop("remove", f"this {trailing_comma}",)
644 |+kwargs.pop("remove", f"this {trailing_comma}") 640 |+kwargs.pop("remove", f"this {trailing_comma}")
645 645 | 641 641 |
646 646 | raise Exception( 642 642 | raise Exception(
647 647 | "first", extra=f"Add trailing comma here ->" 643 643 | "first", extra=f"Add trailing comma here ->"
COM81.py:647:49: COM812 [*] Trailing comma missing COM81.py:643:49: COM812 [*] Trailing comma missing
| |
646 | raise Exception( 642 | raise Exception(
647 | "first", extra=f"Add trailing comma here ->" 643 | "first", extra=f"Add trailing comma here ->"
| COM812 | COM812
648 | ) 644 | )
| |
= help: Add trailing comma = help: Add trailing comma
Safe fix Safe fix
644 644 | kwargs.pop("remove", f"this {trailing_comma}",) 640 640 | kwargs.pop("remove", f"this {trailing_comma}",)
641 641 |
642 642 | raise Exception(
643 |- "first", extra=f"Add trailing comma here ->"
643 |+ "first", extra=f"Add trailing comma here ->",
644 644 | )
645 645 | 645 645 |
646 646 | raise Exception( 646 646 | assert False, f"<- This is not a trailing comma"
647 |- "first", extra=f"Add trailing comma here ->"
647 |+ "first", extra=f"Add trailing comma here ->",
648 648 | )
649 649 |
650 650 | assert False, f"<- This is not a trailing comma"

View file

@ -0,0 +1,10 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
COM81_syntax_error.py:2:5: SyntaxError: Starred expression cannot be used here
|
1 | (
2 | *args
| ^
3 | )
|

View file

@ -204,12 +204,12 @@ pub(crate) fn test_contents<'a>(
print_syntax_errors(parsed.errors(), path, &locator, &transformed); print_syntax_errors(parsed.errors(), path, &locator, &transformed);
panic!( panic!(
r#"Fixed source has a syntax error where the source document does not. This is a bug in one of the generated fixes: "Fixed source has a syntax error where the source document does not. This is a bug in one of the generated fixes:
{syntax_errors} {syntax_errors}
Last generated fixes: Last generated fixes:
{fixes} {fixes}
Source with applied fixes: Source with applied fixes:
{}"#, {}",
transformed.source_code() transformed.source_code()
); );
} }
@ -228,7 +228,12 @@ Source with applied fixes:
.into_iter() .into_iter()
.map(|diagnostic| { .map(|diagnostic| {
let rule = diagnostic.kind.rule(); let rule = diagnostic.kind.rule();
let fixable = diagnostic.fix.as_ref().is_some_and(|fix| matches!(fix.applicability(), Applicability::Safe | Applicability::Unsafe)); let fixable = diagnostic.fix.as_ref().is_some_and(|fix| {
matches!(
fix.applicability(),
Applicability::Safe | Applicability::Unsafe
)
});
match (fixable, rule.fixable()) { match (fixable, rule.fixable()) {
(true, FixAvailability::Sometimes | FixAvailability::Always) (true, FixAvailability::Sometimes | FixAvailability::Always)
@ -236,28 +241,39 @@ Source with applied fixes:
// Ok // Ok
} }
(true, FixAvailability::None) => { (true, FixAvailability::None) => {
panic!("Rule {rule:?} is marked as non-fixable but it created a fix. Change the `Violation::FIX_AVAILABILITY` to either `FixAvailability::Sometimes` or `FixAvailability::Always`"); panic!(
}, "Rule {rule:?} is marked as non-fixable but it created a fix.
Change the `Violation::FIX_AVAILABILITY` to either \
`FixAvailability::Sometimes` or `FixAvailability::Always`"
);
}
(false, FixAvailability::Always) if source_has_errors => {
// Ok
}
(false, FixAvailability::Always) => { (false, FixAvailability::Always) => {
panic!("Rule {rule:?} is marked to always-fixable but the diagnostic has no fix. Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to either `FixAvailability::Sometimes` or `FixAvailability::None") panic!(
"\
Rule {rule:?} is marked to always-fixable but the diagnostic has no fix.
Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to either \
`FixAvailability::Sometimes` or `FixAvailability::None`"
)
} }
} }
assert!(!(fixable && diagnostic.kind.suggestion.is_none()), "Diagnostic emitted by {rule:?} is fixable but `Violation::fix_title` returns `None`.`"); assert!(
!(fixable && diagnostic.kind.suggestion.is_none()),
"Diagnostic emitted by {rule:?} is fixable but \
`Violation::fix_title` returns `None`"
);
// Not strictly necessary but adds some coverage for this code path // Not strictly necessary but adds some coverage for this code path
let noqa = directives.noqa_line_for.resolve(diagnostic.start()); let noqa = directives.noqa_line_for.resolve(diagnostic.start());
Message::from_diagnostic(diagnostic, source_code.clone(), noqa) Message::from_diagnostic(diagnostic, source_code.clone(), noqa)
}) })
.chain( .chain(parsed.errors().iter().map(|parse_error| {
parsed
.errors()
.iter()
.map(|parse_error| {
Message::from_parse_error(parse_error, &locator, source_code.clone()) Message::from_parse_error(parse_error, &locator, source_code.clone())
}) }))
)
.sorted() .sorted()
.collect(); .collect();
(messages, transformed) (messages, transformed)