Skip to content

Commit e64d772

Browse files
authored
Standardize syntax error construction (#20903)
Summary -- This PR unifies the two different ways Ruff and ty construct syntax errors. Ruff has been storing the primary message in the diagnostic itself, while ty attached the message to the primary annotation: ``` > ruff check try.py invalid-syntax: name capture `x` makes remaining patterns unreachable --> try.py:2:10 | 1 | match 42: 2 | case x: ... | ^ 3 | case y: ... | Found 1 error. > uvx ty check try.py WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. Checking ------------------------------------------------------------ 1/1 files error[invalid-syntax] --> try.py:2:10 | 1 | match 42: 2 | case x: ... | ^ name capture `x` makes remaining patterns unreachable 3 | case y: ... | Found 1 diagnostic ``` I think there are benefits to both approaches, and I do like ty's version, but I feel like we should pick one (and it might help with #20901 eventually). I slightly prefer Ruff's version, so I went with that. Hopefully this isn't too controversial, but I'm happy to close this if it is. Note that this shouldn't change any other diagnostic formats in ty because [`Diagnostic::primary_message`](https://github.com/astral-sh/ruff/blob/98d27c412810e157f8a65ea75726d66676628225/crates/ruff_db/src/diagnostic/mod.rs#L177) was already falling back to the primary annotation message if the diagnostic message was empty. As a result, I think this change will partially resolve the FIXME therein. Test Plan -- Existing tests with updated snapshots
1 parent 0369668 commit e64d772

File tree

9 files changed

+43
-72
lines changed

9 files changed

+43
-72
lines changed

crates/ruff/src/diagnostics.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ use log::{debug, warn};
1313
use ruff_db::diagnostic::Diagnostic;
1414
use ruff_linter::codes::Rule;
1515
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
16-
use ruff_linter::message::create_syntax_error_diagnostic;
1716
use ruff_linter::package::PackageRoot;
1817
use ruff_linter::pyproject_toml::lint_pyproject_toml;
1918
use ruff_linter::settings::types::UnsafeFixes;
@@ -103,11 +102,7 @@ impl Diagnostics {
103102
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
104103
let dummy = SourceFileBuilder::new(name, "").finish();
105104
Self::new(
106-
vec![create_syntax_error_diagnostic(
107-
dummy,
108-
err,
109-
TextRange::default(),
110-
)],
105+
vec![Diagnostic::invalid_syntax(dummy, err, TextRange::default())],
111106
FxHashMap::default(),
112107
)
113108
}

crates/ruff_db/src/diagnostic/mod.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,14 @@ impl Diagnostic {
8484
/// at time of writing, `ruff_db` depends on `ruff_python_parser` instead of
8585
/// the other way around. And since we want to do this conversion in a couple
8686
/// places, it makes sense to centralize it _somewhere_. So it's here for now.
87-
///
88-
/// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic
89-
/// message.
9087
pub fn invalid_syntax(
9188
span: impl Into<Span>,
9289
message: impl IntoDiagnosticMessage,
9390
range: impl Ranged,
9491
) -> Diagnostic {
95-
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
92+
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
9693
let span = span.into().with_range(range.range());
97-
diag.annotate(Annotation::primary(span).message(message));
94+
diag.annotate(Annotation::primary(span));
9895
diag
9996
}
10097

crates/ruff_linter/src/linter.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ use crate::checkers::tokens::check_tokens;
2424
use crate::directives::Directives;
2525
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
2626
use crate::fix::{FixResult, fix_file};
27-
use crate::message::create_syntax_error_diagnostic;
2827
use crate::noqa::add_noqa;
2928
use crate::package::PackageRoot;
3029
use crate::registry::Rule;
@@ -496,15 +495,15 @@ fn diagnostics_to_messages(
496495
parse_errors
497496
.iter()
498497
.map(|parse_error| {
499-
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
498+
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
500499
})
501500
.chain(unsupported_syntax_errors.iter().map(|syntax_error| {
502-
create_syntax_error_diagnostic(source_file.clone(), syntax_error, syntax_error)
501+
Diagnostic::invalid_syntax(source_file.clone(), syntax_error, syntax_error)
503502
}))
504503
.chain(
505504
semantic_syntax_errors
506505
.iter()
507-
.map(|error| create_syntax_error_diagnostic(source_file.clone(), error, error)),
506+
.map(|error| Diagnostic::invalid_syntax(source_file.clone(), error, error)),
508507
)
509508
.chain(diagnostics.into_iter().map(|mut diagnostic| {
510509
if let Some(range) = diagnostic.range() {

crates/ruff_linter/src/message/mod.rs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use ruff_db::files::File;
1616
pub use grouped::GroupedEmitter;
1717
use ruff_notebook::NotebookIndex;
1818
use ruff_source_file::{SourceFile, SourceFileBuilder};
19-
use ruff_text_size::{Ranged, TextRange, TextSize};
19+
use ruff_text_size::{TextRange, TextSize};
2020
pub use sarif::SarifEmitter;
2121

2222
use crate::Fix;
@@ -26,24 +26,6 @@ use crate::settings::types::{OutputFormat, RuffOutputFormat};
2626
mod grouped;
2727
mod sarif;
2828

29-
/// Creates a `Diagnostic` from a syntax error, with the format expected by Ruff.
30-
///
31-
/// This is almost identical to `ruff_db::diagnostic::create_syntax_error_diagnostic`, except the
32-
/// `message` is stored as the primary diagnostic message instead of on the primary annotation.
33-
///
34-
/// TODO(brent) These should be unified at some point, but we keep them separate for now to avoid a
35-
/// ton of snapshot changes while combining ruff's diagnostic type with `Diagnostic`.
36-
pub fn create_syntax_error_diagnostic(
37-
span: impl Into<Span>,
38-
message: impl std::fmt::Display,
39-
range: impl Ranged,
40-
) -> Diagnostic {
41-
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
42-
let span = span.into().with_range(range.range());
43-
diag.annotate(Annotation::primary(span));
44-
diag
45-
}
46-
4729
/// Create a `Diagnostic` from a panic.
4830
pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagnostic {
4931
let mut diagnostic = Diagnostic::new(
@@ -260,8 +242,6 @@ mod tests {
260242
use crate::message::{Emitter, EmitterContext, create_lint_diagnostic};
261243
use crate::{Edit, Fix};
262244

263-
use super::create_syntax_error_diagnostic;
264-
265245
pub(super) fn create_syntax_error_diagnostics() -> Vec<Diagnostic> {
266246
let source = r"from os import
267247
@@ -274,7 +254,7 @@ if call(foo
274254
.errors()
275255
.iter()
276256
.map(|parse_error| {
277-
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
257+
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
278258
})
279259
.collect()
280260
}

crates/ruff_linter/src/test.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use ruff_source_file::SourceFileBuilder;
2626
use crate::codes::Rule;
2727
use crate::fix::{FixResult, fix_file};
2828
use crate::linter::check_path;
29-
use crate::message::{EmitterContext, create_syntax_error_diagnostic};
29+
use crate::message::EmitterContext;
3030
use crate::package::PackageRoot;
3131
use crate::packaging::detect_package_root;
3232
use crate::settings::types::UnsafeFixes;
@@ -405,7 +405,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
405405
diagnostic
406406
})
407407
.chain(parsed.errors().iter().map(|parse_error| {
408-
create_syntax_error_diagnostic(source_code.clone(), &parse_error.error, parse_error)
408+
Diagnostic::invalid_syntax(source_code.clone(), &parse_error.error, parse_error)
409409
}))
410410
.sorted_by(Diagnostic::ruff_start_ordering)
411411
.collect();
@@ -419,7 +419,7 @@ fn print_syntax_errors(errors: &[ParseError], path: &Path, source: &SourceKind)
419419
let messages: Vec<_> = errors
420420
.iter()
421421
.map(|parse_error| {
422-
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
422+
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
423423
})
424424
.collect();
425425

crates/ty/tests/cli/main.rs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -92,42 +92,42 @@ fn test_quiet_output() -> anyhow::Result<()> {
9292
#[test]
9393
fn test_run_in_sub_directory() -> anyhow::Result<()> {
9494
let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?;
95-
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r###"
95+
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r"
9696
success: false
9797
exit_code: 1
9898
----- stdout -----
99-
error[invalid-syntax]
99+
error[invalid-syntax]: Expected an expression
100100
--> <temp_dir>/test.py:1:2
101101
|
102102
1 | ~
103-
| ^ Expected an expression
103+
| ^
104104
|
105105
106106
Found 1 diagnostic
107107
108108
----- stderr -----
109-
"###);
109+
");
110110
Ok(())
111111
}
112112

113113
#[test]
114114
fn test_include_hidden_files_by_default() -> anyhow::Result<()> {
115115
let case = CliTest::with_files([(".test.py", "~")])?;
116-
assert_cmd_snapshot!(case.command(), @r###"
116+
assert_cmd_snapshot!(case.command(), @r"
117117
success: false
118118
exit_code: 1
119119
----- stdout -----
120-
error[invalid-syntax]
120+
error[invalid-syntax]: Expected an expression
121121
--> .test.py:1:2
122122
|
123123
1 | ~
124-
| ^ Expected an expression
124+
| ^
125125
|
126126
127127
Found 1 diagnostic
128128
129129
----- stderr -----
130-
"###);
130+
");
131131
Ok(())
132132
}
133133

@@ -146,57 +146,57 @@ fn test_respect_ignore_files() -> anyhow::Result<()> {
146146
"###);
147147

148148
// Test that we can set to false via CLI
149-
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r###"
149+
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
150150
success: false
151151
exit_code: 1
152152
----- stdout -----
153-
error[invalid-syntax]
153+
error[invalid-syntax]: Expected an expression
154154
--> test.py:1:2
155155
|
156156
1 | ~
157-
| ^ Expected an expression
157+
| ^
158158
|
159159
160160
Found 1 diagnostic
161161
162162
----- stderr -----
163-
"###);
163+
");
164164

165165
// Test that we can set to false via config file
166166
case.write_file("ty.toml", "src.respect-ignore-files = false")?;
167-
assert_cmd_snapshot!(case.command(), @r###"
167+
assert_cmd_snapshot!(case.command(), @r"
168168
success: false
169169
exit_code: 1
170170
----- stdout -----
171-
error[invalid-syntax]
171+
error[invalid-syntax]: Expected an expression
172172
--> test.py:1:2
173173
|
174174
1 | ~
175-
| ^ Expected an expression
175+
| ^
176176
|
177177
178178
Found 1 diagnostic
179179
180180
----- stderr -----
181-
"###);
181+
");
182182

183183
// Ensure CLI takes precedence
184184
case.write_file("ty.toml", "src.respect-ignore-files = true")?;
185-
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r###"
185+
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
186186
success: false
187187
exit_code: 1
188188
----- stdout -----
189-
error[invalid-syntax]
189+
error[invalid-syntax]: Expected an expression
190190
--> test.py:1:2
191191
|
192192
1 | ~
193-
| ^ Expected an expression
193+
| ^
194194
|
195195
196196
Found 1 diagnostic
197197
198198
----- stderr -----
199-
"###);
199+
");
200200
Ok(())
201201
}
202202

crates/ty/tests/cli/python_environment.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -655,15 +655,15 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
655655
),
656656
])?;
657657

658-
assert_cmd_snapshot!(case.command(), @r###"
658+
assert_cmd_snapshot!(case.command(), @r#"
659659
success: false
660660
exit_code: 1
661661
----- stdout -----
662-
error[invalid-syntax]
662+
error[invalid-syntax]: Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
663663
--> test.py:2:1
664664
|
665665
2 | match object():
666-
| ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
666+
| ^^^^^
667667
3 | case int():
668668
4 | pass
669669
|
@@ -678,17 +678,17 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
678678
Found 1 diagnostic
679679
680680
----- stderr -----
681-
"###);
681+
"#);
682682

683-
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r###"
683+
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
684684
success: false
685685
exit_code: 1
686686
----- stdout -----
687-
error[invalid-syntax]
687+
error[invalid-syntax]: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
688688
--> test.py:2:1
689689
|
690690
2 | match object():
691-
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
691+
| ^^^^^
692692
3 | case int():
693693
4 | pass
694694
|
@@ -697,7 +697,7 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
697697
Found 1 diagnostic
698698
699699
----- stderr -----
700-
"###);
700+
");
701701

702702
Ok(())
703703
}

crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_erro…_-_Semantic_syntax_erro…_-_`async`_comprehensio…_-_Python_3.10_(96aa8ec77d46553d).snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syn
3333
# Diagnostics
3434

3535
```
36-
error[invalid-syntax]
36+
error[invalid-syntax]: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
3737
--> src/mdtest_snippet.py:6:19
3838
|
3939
4 | async def f():
4040
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax…
4141
6 | return {n: [x async for x in elements(n)] for n in range(3)}
42-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
42+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
4343
7 | async def test():
4444
8 | # error: [not-iterable] "Object of type `range` is not async-iterable"
4545
|

crates/ty_python_semantic/resources/mdtest/snapshots/version_related_synt…_-_Version-related_synt…_-_`match`_statement_-_Before_3.10_(2545eaa83b635b8b).snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/version_rela
2020
# Diagnostics
2121

2222
```
23-
error[invalid-syntax]
23+
error[invalid-syntax]: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
2424
--> src/mdtest_snippet.py:1:1
2525
|
2626
1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
27-
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
27+
| ^^^^^
2828
2 | case 1:
2929
3 | print("it's one")
3030
|

0 commit comments

Comments
 (0)