diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F722.py b/crates/ruff/resources/test/fixtures/pyflakes/F722.py index 3ff1e983f3..35231d60af 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F722.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F722.py @@ -8,3 +8,6 @@ def f() -> "A": def g() -> "///": pass + + +X: """List[int]"""'☃' = [] diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index c9c795e91b..d72ccba5f2 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -5216,7 +5216,8 @@ impl<'a> Checker<'a> { continue; } - let body = str::raw_contents(contents); + // SAFETY: Safe for docstrings that pass `should_ignore_docstring`. + let body = str::raw_contents(contents).unwrap(); let docstring = Docstring { kind: definition.kind, expr, diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 3d5d1cb1a8..6f0eb768ee 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -30,7 +30,7 @@ pub fn remove_unused_format_arguments_from_dict( !matches!(e, DictElement::Simple { key: Expression::SimpleString(name), .. - } if unused_arguments.contains(&raw_contents(name.value))) + } if raw_contents(name.value).map_or(false, |name| unused_arguments.contains(&name))) }); let mut state = CodegenState { diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F722_F722.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F722_F722.py.snap index 7abd45ef9f..57bed07018 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F722_F722.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F722_F722.py.snap @@ -15,4 +15,17 @@ expression: diagnostics column: 16 fix: ~ parent: ~ +- kind: + name: ForwardAnnotationSyntaxError + body: "Syntax error in forward annotation: `List[int]☃`" + suggestion: ~ + fixable: false + location: + row: 13 + column: 3 + end_location: + row: 13 + column: 21 + fix: ~ + parent: ~ diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index 962a61a218..000cc086e4 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -17,25 +17,13 @@ pub const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ const TRIPLE_QUOTE_SUFFIXES: &[&str] = &["\"\"\"", "'''"]; const SINGLE_QUOTE_SUFFIXES: &[&str] = &["\"", "'"]; -/// Strip the leading and trailing quotes from a docstring. -pub fn raw_contents(contents: &str) -> &str { - for pattern in TRIPLE_QUOTE_STR_PREFIXES - .iter() - .chain(TRIPLE_QUOTE_BYTE_PREFIXES) - { - if contents.starts_with(pattern) { - return &contents[pattern.len()..contents.len() - 3]; - } - } - for pattern in SINGLE_QUOTE_STR_PREFIXES - .iter() - .chain(SINGLE_QUOTE_BYTE_PREFIXES) - { - if contents.starts_with(pattern) { - return &contents[pattern.len()..contents.len() - 1]; - } - } - unreachable!("Expected docstring to start with a valid triple- or single-quote prefix") +/// Strip the leading and trailing quotes from a string. +/// Assumes that the string is a valid string literal, but does not verify that the string +/// is a "simple" string literal (i.e., that it does not contain any implicit concatenations). +pub fn raw_contents(contents: &str) -> Option<&str> { + let leading_quote_str = leading_quote(contents)?; + let trailing_quote_str = trailing_quote(contents)?; + Some(&contents[leading_quote_str.len()..contents.len() - trailing_quote_str.len()]) } /// Return the leading quote for a string or byte literal (e.g., `"""`). diff --git a/crates/ruff_python_ast/src/typing.rs b/crates/ruff_python_ast/src/typing.rs index ee01f7b4bb..11db4d2e46 100644 --- a/crates/ruff_python_ast/src/typing.rs +++ b/crates/ruff_python_ast/src/typing.rs @@ -93,15 +93,14 @@ pub fn parse_type_annotation( locator: &Locator, ) -> Result<(Expr, AnnotationKind)> { let expression = locator.slice(range); - let body = str::raw_contents(expression); - if body == value { + if str::raw_contents(expression).map_or(false, |body| body == value) { // The annotation is considered "simple" if and only if the raw representation (e.g., // `List[int]` within "List[int]") exactly matches the parsed representation. This // isn't the case, e.g., for implicit concatenations, or for annotations that contain // escaped quotes. let leading_quote = str::leading_quote(expression).unwrap(); let expr = parser::parse_expression_located( - body, + value, "", Location::new( range.location.row(),