[pyflakes] Show syntax error message for F722 (#15523)

## Summary

Ref: https://github.com/astral-sh/ruff/pull/15387#discussion_r1917796907

This PR updates `F722` to show syntax error message instead of the
string content.

I think it's more useful to show the syntax error message than the
string content. In the future, when the diagnostics renderer is more
capable, we could even highlight the exact location of the syntax error
along with the annotation string.

This is also in line with how we show the diagnostic in red knot.

## Test Plan

Update existing test snapshots.
This commit is contained in:
Dhruv Manilawala 2025-01-16 12:44:01 +05:30 committed by GitHub
parent cf4ab7cba1
commit 79e52c7fdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 80 additions and 88 deletions

View file

@ -153,9 +153,7 @@ pub(crate) fn parse_string_annotation(
} else if raw_contents(node_text)
.is_some_and(|raw_contents| raw_contents == string_literal.as_str())
{
let parsed =
ruff_python_parser::parse_string_annotation(source.as_str(), string_literal);
match parsed {
match ruff_python_parser::parse_string_annotation(source.as_str(), string_literal) {
Ok(parsed) => return Some(parsed),
Err(parse_error) => context.report_lint(
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,

View file

@ -49,7 +49,7 @@ use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_index::Indexer;
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind, ParsedAnnotation};
use ruff_python_parser::{Parsed, Tokens};
use ruff_python_parser::{ParseError, Parsed, Tokens};
use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags};
use ruff_python_semantic::analyze::{imports, typing};
use ruff_python_semantic::{
@ -234,7 +234,7 @@ impl<'a> Checker<'a> {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
parsed: &'a Parsed<ModModule>,
parsed_annotations_arena: &'a typed_arena::Arena<ParsedAnnotation>,
parsed_annotations_arena: &'a typed_arena::Arena<Result<ParsedAnnotation, ParseError>>,
settings: &'a LinterSettings,
noqa_line_for: &'a NoqaMapping,
noqa: flags::Noqa,
@ -425,7 +425,7 @@ impl<'a> Checker<'a> {
pub(crate) fn parse_type_annotation(
&self,
annotation: &ast::ExprStringLiteral,
) -> Option<&'a ParsedAnnotation> {
) -> Result<&'a ParsedAnnotation, &'a ParseError> {
self.parsed_annotations_cache
.lookup_or_parse(annotation, self.locator.contents())
}
@ -441,7 +441,7 @@ impl<'a> Checker<'a> {
match_fn: impl FnOnce(&ast::Expr) -> bool,
) -> bool {
if let ast::Expr::StringLiteral(string_annotation) = expr {
let Some(parsed_annotation) = self.parse_type_annotation(string_annotation) else {
let Some(parsed_annotation) = self.parse_type_annotation(string_annotation).ok() else {
return false;
};
match_fn(parsed_annotation.expression())
@ -2318,8 +2318,8 @@ impl<'a> Checker<'a> {
while !self.visit.string_type_definitions.is_empty() {
let type_definitions = std::mem::take(&mut self.visit.string_type_definitions);
for (string_expr, snapshot) in type_definitions {
let annotation_parse_result = self.parse_type_annotation(string_expr);
if let Some(parsed_annotation) = annotation_parse_result {
match self.parse_type_annotation(string_expr) {
Ok(parsed_annotation) => {
self.parsed_type_annotation = Some(parsed_annotation);
let annotation = string_expr.value.to_str();
@ -2327,19 +2327,25 @@ impl<'a> Checker<'a> {
self.semantic.restore(snapshot);
if self.semantic.in_annotation() && self.semantic.in_typing_only_annotation() {
if self.semantic.in_annotation()
&& self.semantic.in_typing_only_annotation()
{
if self.enabled(Rule::QuotedAnnotation) {
pyupgrade::rules::quoted_annotation(self, annotation, range);
}
}
if self.source_type.is_stub() {
if self.enabled(Rule::QuotedAnnotationInStub) {
flake8_pyi::rules::quoted_annotation_in_stub(self, annotation, range);
flake8_pyi::rules::quoted_annotation_in_stub(
self, annotation, range,
);
}
}
let type_definition_flag = match parsed_annotation.kind() {
AnnotationKind::Simple => SemanticModelFlags::SIMPLE_STRING_TYPE_DEFINITION,
AnnotationKind::Simple => {
SemanticModelFlags::SIMPLE_STRING_TYPE_DEFINITION
}
AnnotationKind::Complex => {
SemanticModelFlags::COMPLEX_STRING_TYPE_DEFINITION
}
@ -2360,19 +2366,21 @@ impl<'a> Checker<'a> {
}
}
self.parsed_type_annotation = None;
} else {
}
Err(parse_error) => {
self.semantic.restore(snapshot);
if self.enabled(Rule::ForwardAnnotationSyntaxError) {
self.push_type_diagnostic(Diagnostic::new(
pyflakes::rules::ForwardAnnotationSyntaxError {
body: string_expr.value.to_string(),
parse_error: parse_error.error.to_string(),
},
string_expr.range(),
));
}
}
}
}
// If we're parsing string annotations inside string annotations
// (which is the only reason we might enter a second iteration of this loop),
@ -2541,12 +2549,12 @@ impl<'a> Checker<'a> {
}
struct ParsedAnnotationsCache<'a> {
arena: &'a typed_arena::Arena<ParsedAnnotation>,
by_offset: RefCell<FxHashMap<TextSize, Option<&'a ParsedAnnotation>>>,
arena: &'a typed_arena::Arena<Result<ParsedAnnotation, ParseError>>,
by_offset: RefCell<FxHashMap<TextSize, Result<&'a ParsedAnnotation, &'a ParseError>>>,
}
impl<'a> ParsedAnnotationsCache<'a> {
fn new(arena: &'a typed_arena::Arena<ParsedAnnotation>) -> Self {
fn new(arena: &'a typed_arena::Arena<Result<ParsedAnnotation, ParseError>>) -> Self {
Self {
arena,
by_offset: RefCell::default(),
@ -2557,17 +2565,15 @@ impl<'a> ParsedAnnotationsCache<'a> {
&self,
annotation: &ast::ExprStringLiteral,
source: &str,
) -> Option<&'a ParsedAnnotation> {
) -> Result<&'a ParsedAnnotation, &'a ParseError> {
*self
.by_offset
.borrow_mut()
.entry(annotation.start())
.or_insert_with(|| {
if let Ok(annotation) = parse_type_annotation(annotation, source) {
Some(self.arena.alloc(annotation))
} else {
None
}
self.arena
.alloc(parse_type_annotation(annotation, source))
.as_ref()
})
}

View file

@ -519,7 +519,7 @@ fn check_dynamically_typed<F>(
{
if let Expr::StringLiteral(string_expr) = annotation {
// Quoted annotations
if let Some(parsed_annotation) = checker.parse_type_annotation(string_expr) {
if let Ok(parsed_annotation) = checker.parse_type_annotation(string_expr) {
if type_hint_resolves_to_any(
parsed_annotation.expression(),
checker,

View file

@ -24,13 +24,13 @@ use ruff_macros::{derive_message_formats, ViolationMetadata};
/// - [PEP 563 Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/)
#[derive(ViolationMetadata)]
pub(crate) struct ForwardAnnotationSyntaxError {
pub body: String,
pub parse_error: String,
}
impl Violation for ForwardAnnotationSyntaxError {
#[derive_message_formats]
fn message(&self) -> String {
let ForwardAnnotationSyntaxError { body } = self;
format!("Syntax error in forward annotation: `{body}`")
let ForwardAnnotationSyntaxError { parse_error } = self;
format!("Syntax error in forward annotation: {parse_error}")
}
}

View file

@ -1,15 +1,14 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F722.py:9:12: F722 Syntax error in forward annotation: `///`
F722.py:9:12: F722 Syntax error in forward annotation: Expected an expression
|
9 | def g() -> "///":
| ^^^^^ F722
10 | pass
|
F722.py:13:4: F722 Syntax error in forward annotation: `List[int]☃`
F722.py:13:4: F722 Syntax error in forward annotation: Got unexpected token ☃
|
13 | X: """List[int]"""'☃' = []
| ^^^^^^^^^^^^^^^^^^ F722
@ -17,10 +16,7 @@ F722.py:13:4: F722 Syntax error in forward annotation: `List[int]☃`
15 | # Type annotations with triple quotes can contain newlines and indentation
|
F722.py:30:11: F722 Syntax error in forward annotation: `
int |
str)
`
F722.py:30:11: F722 Syntax error in forward annotation: Unexpected token at the end of an expression
|
28 | """
29 |
@ -34,10 +30,7 @@ str)
35 | invalid2: """
|
F722.py:35:11: F722 Syntax error in forward annotation: `
int) |
str
`
F722.py:35:11: F722 Syntax error in forward annotation: Unexpected token at the end of an expression
|
33 | """
34 |
@ -51,9 +44,7 @@ str
40 | ((int)
|
F722.py:39:11: F722 Syntax error in forward annotation: `
((int)
`
F722.py:39:11: F722 Syntax error in forward annotation: unexpected EOF while parsing
|
37 | str
38 | """
@ -66,9 +57,7 @@ F722.py:39:11: F722 Syntax error in forward annotation: `
43 | (int
|
F722.py:42:11: F722 Syntax error in forward annotation: `
(int
`
F722.py:42:11: F722 Syntax error in forward annotation: unexpected EOF while parsing
|
40 | ((int)
41 | """

View file

@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F722_1.py:8:22: F722 Syntax error in forward annotation: `this isn't python`
F722_1.py:8:22: F722 Syntax error in forward annotation: Unexpected token at the end of an expression
|
6 | @no_type_check
7 | class C:
@ -11,7 +10,7 @@ F722_1.py:8:22: F722 Syntax error in forward annotation: `this isn't python`
9 | x: "this also isn't python" = 1
|
F722_1.py:8:46: F722 Syntax error in forward annotation: `this isn't python either`
F722_1.py:8:46: F722 Syntax error in forward annotation: Unexpected token at the end of an expression
|
6 | @no_type_check
7 | class C:
@ -20,7 +19,7 @@ F722_1.py:8:46: F722 Syntax error in forward annotation: `this isn't python eith
9 | x: "this also isn't python" = 1
|
F722_1.py:9:12: F722 Syntax error in forward annotation: `this also isn't python`
F722_1.py:9:12: F722 Syntax error in forward annotation: Unexpected token at the end of an expression
|
7 | class C:
8 | def f(self, arg: "this isn't python") -> "this isn't python either":

View file

@ -179,7 +179,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, parameters: &Parameters)
if let Expr::StringLiteral(string_expr) = annotation.as_ref() {
// Quoted annotation.
if let Some(parsed_annotation) = checker.parse_type_annotation(string_expr) {
if let Ok(parsed_annotation) = checker.parse_type_annotation(string_expr) {
let Some(expr) = type_hint_explicitly_allows_none(
parsed_annotation.expression(),
checker,

View file

@ -109,7 +109,7 @@ impl<'a> TypingTarget<'a> {
Expr::NoneLiteral(_) => Some(TypingTarget::None),
Expr::StringLiteral(string_expr) => checker
.parse_type_annotation(string_expr)
.as_ref()
.ok()
.map(|parsed_annotation| {
TypingTarget::ForwardReference(parsed_annotation.expression())
}),