mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:25:17 +00:00
[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:
parent
cf4ab7cba1
commit
79e52c7fdf
8 changed files with 80 additions and 88 deletions
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 | """
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
}),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue