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 d900db4c7e..d6c3f9e298 100644 --- a/crates/red_knot_python_semantic/src/types/string_annotation.rs +++ b/crates/red_knot_python_semantic/src/types/string_annotation.rs @@ -1,5 +1,4 @@ use ruff_db::source::source_text; -use ruff_python_ast::str::raw_contents; use ruff_python_ast::{self as ast, ModExpression}; use ruff_python_parser::Parsed; use ruff_text_size::Ranged; @@ -138,9 +137,8 @@ pub(crate) fn parse_string_annotation( let _span = tracing::trace_span!("parse_string_annotation", string=?string_expr.range(), file=%file.path(db)).entered(); let source = source_text(db.upcast(), file); - let node_text = &source[string_expr.range()]; - if let [string_literal] = string_expr.value.as_slice() { + if let Some(string_literal) = string_expr.as_unconcatenated_literal() { let prefix = string_literal.flags.prefix(); if prefix.is_raw() { context.report_lint( @@ -150,9 +148,7 @@ pub(crate) fn parse_string_annotation( ); // Compare the raw contents (without quotes) of the expression with the parsed contents // contained in the string literal. - } else if raw_contents(node_text) - .is_some_and(|raw_contents| raw_contents == string_literal.as_str()) - { + } else if &source[string_literal.content_range()] == string_literal.as_str() { match ruff_python_parser::parse_string_annotation(source.as_str(), string_literal) { Ok(parsed) => return Some(parsed), Err(parse_error) => context.report_lint( diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index ae428cbf46..54e2350cd5 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -182,9 +182,8 @@ pub(crate) fn definitions(checker: &mut Checker) { continue; }; - // If the `ExprStringLiteral` has multiple parts, it is implicitly concatenated. - // We don't support recognising such strings as docstrings in our model currently. - let [sole_string_part] = string_literal.value.as_slice() else { + // We don't recognise implicitly concatenated strings as valid docstrings in our model currently. + let Some(sole_string_part) = string_literal.as_unconcatenated_literal() else { #[allow(deprecated)] let location = checker .locator diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index b4a594ead1..9226ced744 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1537,7 +1537,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } } if checker.enabled(Rule::MissingFStringSyntax) { - for string_literal in value.as_slice() { + for string_literal in value { ruff::rules::missing_fstring_syntax(checker, string_literal); } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs index c475d7a9c3..f6af57b493 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs @@ -159,11 +159,17 @@ fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option } Ordering::Equal => { let list_items: Vec<&str> = vec![str_value.to_str()]; - Some(construct_replacement(&list_items, str_value.flags())) + Some(construct_replacement( + &list_items, + str_value.first_literal_flags(), + )) } Ordering::Less => { let list_items: Vec<&str> = str_value.to_str().split_whitespace().collect(); - Some(construct_replacement(&list_items, str_value.flags())) + Some(construct_replacement( + &list_items, + str_value.first_literal_flags(), + )) } } } @@ -187,7 +193,7 @@ fn split_sep( } }; - construct_replacement(&list_items, str_value.flags()) + construct_replacement(&list_items, str_value.first_literal_flags()) } /// Returns the value of the `maxsplit` argument as an `i32`, if it is a numeric value. diff --git a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs index eccba401f5..309ec0ccc7 100644 --- a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs @@ -72,7 +72,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option< if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr { if flags.is_none() { // take the flags from the first Expr - flags = Some(value.flags()); + flags = Some(value.first_literal_flags()); } Some(value.to_str()) } else { diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 7f2bde1e3b..f831ff314c 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -1287,6 +1287,17 @@ pub struct ExprStringLiteral { pub value: StringLiteralValue, } +impl ExprStringLiteral { + /// Return `Some(literal)` if the string only consists of a single `StringLiteral` part + /// (indicating that it is not implicitly concatenated). Otherwise, return `None`. + pub fn as_unconcatenated_literal(&self) -> Option<&StringLiteral> { + match &self.value.inner { + StringLiteralValueInner::Single(value) => Some(value), + StringLiteralValueInner::Concatenated(_) => None, + } + } +} + /// The value representing a [`ExprStringLiteral`]. #[derive(Clone, Debug, PartialEq)] pub struct StringLiteralValue { @@ -1304,7 +1315,7 @@ impl StringLiteralValue { /// Returns the [`StringLiteralFlags`] associated with this string literal. /// /// For an implicitly concatenated string, it returns the flags for the first literal. - pub fn flags(&self) -> StringLiteralFlags { + pub fn first_literal_flags(&self) -> StringLiteralFlags { self.iter() .next() .expect( @@ -1485,8 +1496,8 @@ bitflags! { /// /// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix /// from an existing string literal, consider passing along the [`StringLiteral::flags`] field or -/// the result of the [`StringLiteralValue::flags`] method. If you don't have an existing string but -/// have a `Checker` from the `ruff_linter` crate available, consider using +/// the result of the [`StringLiteralValue::first_literal_flags`] method. If you don't have an +/// existing string but have a `Checker` from the `ruff_linter` crate available, consider using /// `Checker::default_string_flags` to create instances of this struct; this method will properly /// handle surrounding f-strings. For usage that doesn't fit into one of these categories, the /// public constructor [`StringLiteralFlags::empty`] can be used. @@ -1791,16 +1802,6 @@ impl BytesLiteralValue { pub fn bytes(&self) -> impl Iterator + '_ { self.iter().flat_map(|part| part.as_slice().iter().copied()) } - - /// Returns the [`BytesLiteralFlags`] associated with this literal. - /// - /// For an implicitly concatenated literal, it returns the flags for the first literal. - pub fn flags(&self) -> BytesLiteralFlags { - self.iter() - .next() - .expect("There should always be at least one literal in an `ExprBytesLiteral` node") - .flags - } } impl<'a> IntoIterator for &'a BytesLiteralValue { @@ -1890,12 +1891,11 @@ bitflags! { /// ## Notes on usage /// /// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix -/// from an existing bytes literal, consider passing along the [`BytesLiteral::flags`] field or the -/// result of the [`BytesLiteralValue::flags`] method. If you don't have an existing literal but -/// have a `Checker` from the `ruff_linter` crate available, consider using -/// `Checker::default_bytes_flags` to create instances of this struct; this method will properly -/// handle surrounding f-strings. For usage that doesn't fit into one of these categories, the -/// public constructor [`BytesLiteralFlags::empty`] can be used. +/// from an existing bytes literal, consider passing along the [`BytesLiteral::flags`] field. If +/// you don't have an existing literal but have a `Checker` from the `ruff_linter` crate available, +/// consider using `Checker::default_bytes_flags` to create instances of this struct; this method +/// will properly handle surrounding f-strings. For usage that doesn't fit into one of these +/// categories, the public constructor [`BytesLiteralFlags::empty`] can be used. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct BytesLiteralFlags(BytesLiteralFlagsInner); diff --git a/crates/ruff_python_formatter/src/expression/expr_string_literal.rs b/crates/ruff_python_formatter/src/expression/expr_string_literal.rs index 7dcb5ffaf2..c000a490f4 100644 --- a/crates/ruff_python_formatter/src/expression/expr_string_literal.rs +++ b/crates/ruff_python_formatter/src/expression/expr_string_literal.rs @@ -28,9 +28,7 @@ impl FormatRuleWithOptions> for FormatExp impl FormatNodeRule for FormatExprStringLiteral { fn fmt_fields(&self, item: &ExprStringLiteral, f: &mut PyFormatter) -> FormatResult<()> { - let ExprStringLiteral { value, .. } = item; - - if let [string_literal] = value.as_slice() { + if let Some(string_literal) = item.as_unconcatenated_literal() { string_literal.format().with_options(self.kind).fmt(f) } else { // Always join strings that aren't parenthesized and thus, always on a single line. diff --git a/crates/ruff_python_parser/src/typing.rs b/crates/ruff_python_parser/src/typing.rs index ffc7dce741..5111eac646 100644 --- a/crates/ruff_python_parser/src/typing.rs +++ b/crates/ruff_python_parser/src/typing.rs @@ -1,7 +1,6 @@ //! This module takes care of parsing a type annotation. use ruff_python_ast::relocate::relocate_expr; -use ruff_python_ast::str::raw_contents; use ruff_python_ast::{Expr, ExprStringLiteral, ModExpression, StringLiteral}; use ruff_text_size::Ranged; @@ -57,14 +56,10 @@ pub fn parse_type_annotation( string_expr: &ExprStringLiteral, source: &str, ) -> AnnotationParseResult { - let expr_text = &source[string_expr.range()]; - - if let [string_literal] = string_expr.value.as_slice() { + if let Some(string_literal) = string_expr.as_unconcatenated_literal() { // Compare the raw contents (without quotes) of the expression with the parsed contents // contained in the string literal. - if raw_contents(expr_text) - .is_some_and(|raw_contents| raw_contents == string_literal.as_str()) - { + if &source[string_literal.content_range()] == string_literal.as_str() { parse_simple_type_annotation(string_literal, source) } else { // The raw contents of the string doesn't match the parsed content. This could be the