diff --git a/crates/red_knot_python_semantic/src/types/string_annotation.rs b/crates/red_knot_python_semantic/src/types/string_annotation.rs index e6ee75b95b71f..390a2e62933f2 100644 --- a/crates/red_knot_python_semantic/src/types/string_annotation.rs +++ b/crates/red_knot_python_semantic/src/types/string_annotation.rs @@ -1,7 +1,7 @@ use ruff_db::source::source_text; use ruff_python_ast::str::raw_contents; use ruff_python_ast::{self as ast, ModExpression, StringFlags}; -use ruff_python_parser::{parse_parenthesized_expression_range, Parsed}; +use ruff_python_parser::{parse_expression_range, parse_parenthesized_expression_range, Parsed}; use ruff_text_size::Ranged; use crate::declare_lint; @@ -158,14 +158,13 @@ pub(crate) fn parse_string_annotation( .add_start(string_literal.flags.opener_len()) .sub_end(string_literal.flags.closer_len()); - // TODO: Support multiline strings like: - // ```py - // x: """ - // int - // | float - // """ = 1 - // ``` - match parse_parenthesized_expression_range(source.as_str(), range_excluding_quotes) { + let parsed = if string_literal.flags.is_triple_quoted() { + parse_parenthesized_expression_range(source.as_str(), range_excluding_quotes) + } else { + parse_expression_range(source.as_str(), range_excluding_quotes) + }; + + match parsed { Ok(parsed) => return Some(parsed), Err(parse_error) => context.report_lint( &INVALID_SYNTAX_IN_FORWARD_ANNOTATION, diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F722.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F722.py index 1f5b3d1f9229b..0c7064c75149e 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyflakes/F722.py +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F722.py @@ -24,3 +24,6 @@ def g() -> "///": str ) """ + +# single quotes are not implicitly parenthesized +invalid: "\n int" diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_2.pyi b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_2.pyi index 8456637581f11..c7071fb7528ac 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_2.pyi +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_2.pyi @@ -33,9 +33,6 @@ a: '''\\ list[int]''' = [42] -# TODO: These are valid too. String annotations are assumed to be enclosed in parentheses. -# https://github.com/astral-sh/ruff/issues/9467 - def f(a: ''' list[int] ''' = []): ... diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap index 44141349315c9..6e9c16b8cb024 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap @@ -16,3 +16,11 @@ F722.py:13:4: F722 Syntax error in forward annotation: `List[int]☃` 14 | 15 | y: """ | + +F722.py:29:10: F722 Syntax error in forward annotation: ` + int` + | +28 | # single quotes are not implicitly parenthesized +29 | invalid: "\n int" + | ^^^^^^^^ F722 + | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap index 1519dba9d7602..210f14e1dda1f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap @@ -159,4 +159,75 @@ UP037_2.pyi:32:4: UP037 [*] Remove quotes from type annotation 33 |+list[int]) = [42] 34 34 | 35 35 | -36 36 | # TODO: These are valid too. String annotations are assumed to be enclosed in parentheses. +36 36 | def f(a: ''' + +UP037_2.pyi:36:10: UP037 [*] Remove quotes from type annotation + | +36 | def f(a: ''' + | __________^ +37 | | list[int] +38 | | ''' = []): ... + | |_______^ UP037 + | + = help: Remove quotes + +ℹ Safe fix +33 33 | list[int]''' = [42] +34 34 | +35 35 | +36 |-def f(a: ''' + 36 |+def f(a: +37 37 | list[int] +38 |- ''' = []): ... + 38 |+ = []): ... +39 39 | +40 40 | +41 41 | def f(a: Foo[''' + +UP037_2.pyi:41:14: UP037 [*] Remove quotes from type annotation + | +41 | def f(a: Foo[''' + | ______________^ +42 | | Bar +43 | | [ +44 | | Multi | +45 | | Line +46 | | ] # Comment''']): ... + | |___________________^ UP037 + | + = help: Remove quotes + +ℹ Safe fix +38 38 | ''' = []): ... +39 39 | +40 40 | +41 |-def f(a: Foo[''' + 41 |+def f(a: Foo[( +42 42 | Bar +43 43 | [ +44 44 | Multi | +45 45 | Line +46 |- ] # Comment''']): ... + 46 |+ ] # Comment + 47 |+)]): ... +47 48 | +48 49 | +49 50 | a: '''list + +UP037_2.pyi:49:4: UP037 [*] Remove quotes from type annotation + | +49 | a: '''list + | ____^ +50 | | [int]''' = [42] + | |________^ UP037 + | + = help: Remove quotes + +ℹ Safe fix +46 46 | ] # Comment''']): ... +47 47 | +48 48 | +49 |-a: '''list +50 |-[int]''' = [42] + 49 |+a: (list + 50 |+[int]) = [42] diff --git a/crates/ruff_python_parser/src/typing.rs b/crates/ruff_python_parser/src/typing.rs index 210662a8d7a9e..81ae69d63cb25 100644 --- a/crates/ruff_python_parser/src/typing.rs +++ b/crates/ruff_python_parser/src/typing.rs @@ -5,7 +5,10 @@ use ruff_python_ast::str::raw_contents; use ruff_python_ast::{Expr, ExprStringLiteral, ModExpression, StringFlags, StringLiteral}; use ruff_text_size::Ranged; -use crate::{parse_expression, parse_parenthesized_expression_range, ParseError, Parsed}; +use crate::{ + parse_expression, parse_expression_range, parse_parenthesized_expression_range, ParseError, + Parsed, +}; type AnnotationParseResult = Result; @@ -85,8 +88,13 @@ fn parse_simple_type_annotation( .range() .add_start(string_literal.flags.opener_len()) .sub_end(string_literal.flags.closer_len()); + let parsed = if string_literal.flags.is_triple_quoted() { + parse_parenthesized_expression_range(source, range_excluding_quotes)? + } else { + parse_expression_range(source, range_excluding_quotes)? + }; Ok(ParsedAnnotation { - parsed: parse_parenthesized_expression_range(source, range_excluding_quotes)?, + parsed, kind: AnnotationKind::Simple, }) }