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

View file

@ -519,7 +519,7 @@ fn check_dynamically_typed<F>(
{ {
if let Expr::StringLiteral(string_expr) = annotation { if let Expr::StringLiteral(string_expr) = annotation {
// Quoted annotations // 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( if type_hint_resolves_to_any(
parsed_annotation.expression(), parsed_annotation.expression(),
checker, 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/) /// - [PEP 563 Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/)
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
pub(crate) struct ForwardAnnotationSyntaxError { pub(crate) struct ForwardAnnotationSyntaxError {
pub body: String, pub parse_error: String,
} }
impl Violation for ForwardAnnotationSyntaxError { impl Violation for ForwardAnnotationSyntaxError {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let ForwardAnnotationSyntaxError { body } = self; let ForwardAnnotationSyntaxError { parse_error } = self;
format!("Syntax error in forward annotation: `{body}`") format!("Syntax error in forward annotation: {parse_error}")
} }
} }

View file

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

View file

@ -1,8 +1,7 @@
--- ---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs 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 6 | @no_type_check
7 | class C: 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 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 6 | @no_type_check
7 | class C: 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 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: 7 | class C:
8 | def f(self, arg: "this isn't python") -> "this isn't python either": 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() { if let Expr::StringLiteral(string_expr) = annotation.as_ref() {
// Quoted annotation. // 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( let Some(expr) = type_hint_explicitly_allows_none(
parsed_annotation.expression(), parsed_annotation.expression(),
checker, checker,

View file

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