Skip to content

Commit 74f64d3

Browse files
authored
Server: Allow FixAll action in presence of version-specific syntax errors (#16848)
The single flag `has_syntax_error` on `LinterResult` is replaced with two (private) flags: `has_valid_syntax` and `has_no_unsupported_syntax_errors`, which record whether there are `ParseError`s or `UnsupportedSyntaxError`s, respectively. Only the former is used to prevent a `FixAll` action. An attempt has been made to make consistent the usage of the phrases "valid syntax" (which seems to be used to refer only to _parser_ errors) and "syntax error" (which refers to both _parser_ errors and version-specific syntax errors). Closes #16841
1 parent 999fd4f commit 74f64d3

File tree

5 files changed

+111
-73
lines changed

5 files changed

+111
-73
lines changed

crates/ruff/src/diagnostics.rs

+50-53
Original file line numberDiff line numberDiff line change
@@ -263,48 +263,56 @@ pub(crate) fn lint_path(
263263
};
264264

265265
// Lint the file.
266-
let (
267-
LinterResult {
268-
messages,
269-
has_syntax_error: has_error,
270-
},
271-
transformed,
272-
fixed,
273-
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
274-
if let Ok(FixerResult {
275-
result,
276-
transformed,
277-
fixed,
278-
}) = lint_fix(
279-
path,
280-
package,
281-
noqa,
282-
unsafe_fixes,
283-
settings,
284-
&source_kind,
285-
source_type,
286-
) {
287-
if !fixed.is_empty() {
288-
match fix_mode {
289-
flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?,
290-
flags::FixMode::Diff => {
291-
write!(
292-
&mut io::stdout().lock(),
293-
"{}",
294-
source_kind.diff(&transformed, Some(path)).unwrap()
295-
)?;
266+
let (result, transformed, fixed) =
267+
if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
268+
if let Ok(FixerResult {
269+
result,
270+
transformed,
271+
fixed,
272+
}) = lint_fix(
273+
path,
274+
package,
275+
noqa,
276+
unsafe_fixes,
277+
settings,
278+
&source_kind,
279+
source_type,
280+
) {
281+
if !fixed.is_empty() {
282+
match fix_mode {
283+
flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?,
284+
flags::FixMode::Diff => {
285+
write!(
286+
&mut io::stdout().lock(),
287+
"{}",
288+
source_kind.diff(&transformed, Some(path)).unwrap()
289+
)?;
290+
}
291+
flags::FixMode::Generate => {}
296292
}
297-
flags::FixMode::Generate => {}
298293
}
299-
}
300-
let transformed = if let Cow::Owned(transformed) = transformed {
301-
transformed
294+
let transformed = if let Cow::Owned(transformed) = transformed {
295+
transformed
296+
} else {
297+
source_kind
298+
};
299+
(result, transformed, fixed)
302300
} else {
303-
source_kind
304-
};
305-
(result, transformed, fixed)
301+
// If we fail to fix, lint the original source code.
302+
let result = lint_only(
303+
path,
304+
package,
305+
settings,
306+
noqa,
307+
&source_kind,
308+
source_type,
309+
ParseSource::None,
310+
);
311+
let transformed = source_kind;
312+
let fixed = FxHashMap::default();
313+
(result, transformed, fixed)
314+
}
306315
} else {
307-
// If we fail to fix, lint the original source code.
308316
let result = lint_only(
309317
path,
310318
package,
@@ -317,21 +325,10 @@ pub(crate) fn lint_path(
317325
let transformed = source_kind;
318326
let fixed = FxHashMap::default();
319327
(result, transformed, fixed)
320-
}
321-
} else {
322-
let result = lint_only(
323-
path,
324-
package,
325-
settings,
326-
noqa,
327-
&source_kind,
328-
source_type,
329-
ParseSource::None,
330-
);
331-
let transformed = source_kind;
332-
let fixed = FxHashMap::default();
333-
(result, transformed, fixed)
334-
};
328+
};
329+
330+
let has_error = result.has_syntax_errors();
331+
let messages = result.messages;
335332

336333
if let Some((cache, relative_path, key)) = caching {
337334
// We don't cache parsing errors.

crates/ruff_benchmark/benches/linter.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
8989
);
9090

9191
// Assert that file contains no parse errors
92-
assert!(!result.has_syntax_error);
92+
assert!(!result.has_syntax_errors());
9393
},
9494
criterion::BatchSize::SmallInput,
9595
);

crates/ruff_linter/src/linter.rs

+55-12
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,48 @@ use crate::{directives, fs, Locator};
4040
pub struct LinterResult {
4141
/// A collection of diagnostic messages generated by the linter.
4242
pub messages: Vec<Message>,
43-
/// A flag indicating the presence of syntax errors in the source file.
44-
/// If `true`, at least one syntax error was detected in the source file.
43+
/// Flag indicating that the parsed source code does not contain any
44+
/// [`ParseError`]s
45+
has_valid_syntax: bool,
46+
/// Flag indicating that the parsed source code does not contain any
47+
/// [`UnsupportedSyntaxError`]s
48+
has_no_unsupported_syntax_errors: bool,
49+
}
50+
51+
impl LinterResult {
52+
/// Returns `true` if the parsed source code contains any [`ParseError`]s *or*
53+
/// [`UnsupportedSyntaxError`]s.
54+
///
55+
/// See [`LinterResult::has_invalid_syntax`] for a version specific to [`ParseError`]s.
56+
pub fn has_syntax_errors(&self) -> bool {
57+
!self.has_no_syntax_errors()
58+
}
59+
60+
/// Returns `true` if the parsed source code does not contain any [`ParseError`]s *or*
61+
/// [`UnsupportedSyntaxError`]s.
62+
///
63+
/// See [`LinterResult::has_valid_syntax`] for a version specific to [`ParseError`]s.
64+
pub fn has_no_syntax_errors(&self) -> bool {
65+
self.has_valid_syntax() && self.has_no_unsupported_syntax_errors
66+
}
67+
68+
/// Returns `true` if the parsed source code is valid i.e., it has no [`ParseError`]s.
4569
///
46-
/// This includes both [`ParseError`]s and [`UnsupportedSyntaxError`]s.
47-
pub has_syntax_error: bool,
70+
/// Note that this does not include version-related [`UnsupportedSyntaxError`]s.
71+
///
72+
/// See [`LinterResult::has_no_syntax_errors`] for a version that takes these into account.
73+
pub fn has_valid_syntax(&self) -> bool {
74+
self.has_valid_syntax
75+
}
76+
77+
/// Returns `true` if the parsed source code is invalid i.e., it has [`ParseError`]s.
78+
///
79+
/// Note that this does not include version-related [`UnsupportedSyntaxError`]s.
80+
///
81+
/// See [`LinterResult::has_no_syntax_errors`] for a version that takes these into account.
82+
pub fn has_invalid_syntax(&self) -> bool {
83+
!self.has_valid_syntax()
84+
}
4885
}
4986

5087
pub type FixTable = FxHashMap<Rule, usize>;
@@ -446,7 +483,8 @@ pub fn lint_only(
446483

447484
LinterResult {
448485
messages,
449-
has_syntax_error: parsed.has_syntax_errors(),
486+
has_valid_syntax: parsed.has_valid_syntax(),
487+
has_no_unsupported_syntax_errors: parsed.unsupported_syntax_errors().is_empty(),
450488
}
451489
}
452490

@@ -503,8 +541,11 @@ pub fn lint_fix<'a>(
503541
// As an escape hatch, bail after 100 iterations.
504542
let mut iterations = 0;
505543

506-
// Track whether the _initial_ source code is valid syntax.
507-
let mut is_valid_syntax = false;
544+
// Track whether the _initial_ source code has valid syntax.
545+
let mut has_valid_syntax = false;
546+
547+
// Track whether the _initial_ source code has no unsupported syntax errors.
548+
let mut has_no_unsupported_syntax_errors = false;
508549

509550
let target_version = settings.resolve_target_version(path);
510551

@@ -547,12 +588,13 @@ pub fn lint_fix<'a>(
547588
);
548589

549590
if iterations == 0 {
550-
is_valid_syntax = parsed.has_no_syntax_errors();
591+
has_valid_syntax = parsed.has_valid_syntax();
592+
has_no_unsupported_syntax_errors = parsed.unsupported_syntax_errors().is_empty();
551593
} else {
552-
// If the source code was parseable on the first pass, but is no
553-
// longer parseable on a subsequent pass, then we've introduced a
594+
// If the source code had no syntax errors on the first pass, but
595+
// does on a subsequent pass, then we've introduced a
554596
// syntax error. Return the original code.
555-
if is_valid_syntax {
597+
if has_valid_syntax && has_no_unsupported_syntax_errors {
556598
if let Some(error) = parsed.errors().first() {
557599
report_fix_syntax_error(
558600
path,
@@ -593,7 +635,8 @@ pub fn lint_fix<'a>(
593635
return Ok(FixerResult {
594636
result: LinterResult {
595637
messages,
596-
has_syntax_error: !is_valid_syntax,
638+
has_valid_syntax,
639+
has_no_unsupported_syntax_errors,
597640
},
598641
transformed,
599642
fixed,

crates/ruff_server/src/fix.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
};
1111
use ruff_linter::package::PackageRoot;
1212
use ruff_linter::{
13-
linter::{FixerResult, LinterResult},
13+
linter::FixerResult,
1414
packaging::detect_package_root,
1515
settings::{flags, LinterSettings},
1616
};
@@ -62,9 +62,7 @@ pub(crate) fn fix_all(
6262
// which is inconsistent with how `ruff check --fix` works.
6363
let FixerResult {
6464
transformed,
65-
result: LinterResult {
66-
has_syntax_error, ..
67-
},
65+
result,
6866
..
6967
} = ruff_linter::linter::lint_fix(
7068
&query.virtual_file_path(),
@@ -76,7 +74,7 @@ pub(crate) fn fix_all(
7674
source_type,
7775
)?;
7876

79-
if has_syntax_error {
77+
if result.has_invalid_syntax() {
8078
// If there's a syntax error, then there won't be any fixes to apply.
8179
return Ok(Fixes::default());
8280
}

fuzz/fuzz_targets/ruff_formatter_validity.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ fn do_fuzz(case: &[u8]) -> Corpus {
3737
ParseSource::None,
3838
);
3939

40-
if linter_result.has_syntax_error {
40+
if linter_result.has_syntax_errors() {
4141
return Corpus::Keep; // keep, but don't continue
4242
}
4343

@@ -63,7 +63,7 @@ fn do_fuzz(case: &[u8]) -> Corpus {
6363
);
6464

6565
assert!(
66-
!linter_result.has_syntax_error,
66+
!linter_result.has_syntax_errors(),
6767
"formatter introduced a parse error"
6868
);
6969

0 commit comments

Comments
 (0)