Track quoting style in the tokenizer (#10256)

This commit is contained in:
Alex Waygood 2024-03-08 08:40:06 +00:00 committed by GitHub
parent 72c9f7e4c9
commit c504d7ab11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 4595 additions and 3800 deletions

View file

@ -40,4 +40,7 @@ f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004
# Make sure we do not unescape quotes # Make sure we do not unescape quotes
this_is_fine = "This is an \\'escaped\\' quote" this_is_fine = "This is an \\'escaped\\' quote"
this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
# Invalid escapes in bytestrings are also triggered:
x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004

View file

@ -131,10 +131,7 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer
// For multi-line strings, we expect `noqa` directives on the last line of the // For multi-line strings, we expect `noqa` directives on the last line of the
// string. // string.
Tok::String { Tok::String { kind, .. } if kind.is_triple_quoted() => {
triple_quoted: true,
..
} => {
if locator.contains_line_break(*range) { if locator.contains_line_break(*range) {
string_mappings.push(TextRange::new( string_mappings.push(TextRange::new(
locator.line_start(range.start()), locator.line_start(range.start()),

View file

@ -243,7 +243,7 @@ pub(crate) fn trailing_commas(
// F-strings are handled as `String` token type with the complete range // F-strings are handled as `String` token type with the complete range
// of the outermost f-string. This means that the expression inside the // of the outermost f-string. This means that the expression inside the
// f-string is not checked for trailing commas. // f-string is not checked for trailing commas.
Tok::FStringStart => { Tok::FStringStart(_) => {
fstrings = fstrings.saturating_add(1); fstrings = fstrings.saturating_add(1);
None None
} }

View file

@ -110,7 +110,7 @@ pub(crate) fn implicit(
{ {
let (a_range, b_range) = match (a_tok, b_tok) { let (a_range, b_range) = match (a_tok, b_tok) {
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range), (Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
(Tok::String { .. }, Tok::FStringStart) => { (Tok::String { .. }, Tok::FStringStart(_)) => {
match indexer.fstring_ranges().innermost(b_range.start()) { match indexer.fstring_ranges().innermost(b_range.start()) {
Some(b_range) => (*a_range, b_range), Some(b_range) => (*a_range, b_range),
None => continue, None => continue,
@ -122,7 +122,7 @@ pub(crate) fn implicit(
None => continue, None => continue,
} }
} }
(Tok::FStringEnd, Tok::FStringStart) => { (Tok::FStringEnd, Tok::FStringStart(_)) => {
match ( match (
indexer.fstring_ranges().innermost(a_range.start()), indexer.fstring_ranges().innermost(a_range.start()),
indexer.fstring_ranges().innermost(b_range.start()), indexer.fstring_ranges().innermost(b_range.start()),

View file

@ -1,6 +1,5 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{is_triple_quote, leading_quote};
use ruff_python_parser::lexer::LexResult; use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok; use ruff_python_parser::Tok;
use ruff_source_file::Locator; use ruff_source_file::Locator;
@ -158,7 +157,7 @@ pub(crate) fn avoidable_escaped_quote(
// ```python // ```python
// f'"foo" {'nested'}" // f'"foo" {'nested'}"
// ``` // ```
if matches!(tok, Tok::String { .. } | Tok::FStringStart) { if matches!(tok, Tok::String { .. } | Tok::FStringStart(_)) {
if let Some(fstring_context) = fstrings.last_mut() { if let Some(fstring_context) = fstrings.last_mut() {
fstring_context.ignore_escaped_quotes(); fstring_context.ignore_escaped_quotes();
continue; continue;
@ -170,16 +169,13 @@ pub(crate) fn avoidable_escaped_quote(
Tok::String { Tok::String {
value: string_contents, value: string_contents,
kind, kind,
triple_quoted,
} => { } => {
if kind.is_raw() || *triple_quoted { if kind.is_raw_string() || kind.is_triple_quoted() {
continue; continue;
} }
// Check if we're using the preferred quotation style. // Check if we're using the preferred quotation style.
if !leading_quote(locator.slice(tok_range)).is_some_and(|text| { if Quote::from(kind.quote_style()) != quotes_settings.inline_quotes {
contains_quote(text, quotes_settings.inline_quotes.as_char())
}) {
continue; continue;
} }
@ -192,7 +188,7 @@ pub(crate) fn avoidable_escaped_quote(
let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, tok_range); let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, tok_range);
let fixed_contents = format!( let fixed_contents = format!(
"{prefix}{quote}{value}{quote}", "{prefix}{quote}{value}{quote}",
prefix = kind.as_str(), prefix = kind.prefix_str(),
quote = quotes_settings.inline_quotes.opposite().as_char(), quote = quotes_settings.inline_quotes.opposite().as_char(),
value = unescape_string( value = unescape_string(
string_contents, string_contents,
@ -206,12 +202,11 @@ pub(crate) fn avoidable_escaped_quote(
diagnostics.push(diagnostic); diagnostics.push(diagnostic);
} }
} }
Tok::FStringStart => { Tok::FStringStart(kind) => {
let text = locator.slice(tok_range);
// Check for escaped quote only if we're using the preferred quotation // Check for escaped quote only if we're using the preferred quotation
// style and it isn't a triple-quoted f-string. // style and it isn't a triple-quoted f-string.
let check_for_escaped_quote = !is_triple_quote(text) let check_for_escaped_quote = !kind.is_triple_quoted()
&& contains_quote(text, quotes_settings.inline_quotes.as_char()); && Quote::from(kind.quote_style()) == quotes_settings.inline_quotes;
fstrings.push(FStringContext::new( fstrings.push(FStringContext::new(
check_for_escaped_quote, check_for_escaped_quote,
tok_range, tok_range,
@ -220,9 +215,8 @@ pub(crate) fn avoidable_escaped_quote(
} }
Tok::FStringMiddle { Tok::FStringMiddle {
value: string_contents, value: string_contents,
is_raw, kind,
triple_quoted: _, } if !kind.is_raw_string() => {
} if !is_raw => {
let Some(context) = fstrings.last_mut() else { let Some(context) = fstrings.last_mut() else {
continue; continue;
}; };
@ -315,17 +309,12 @@ pub(crate) fn unnecessary_escaped_quote(
Tok::String { Tok::String {
value: string_contents, value: string_contents,
kind, kind,
triple_quoted,
} => { } => {
if kind.is_raw() || *triple_quoted { if kind.is_raw_string() || kind.is_triple_quoted() {
continue; continue;
} }
let leading = match leading_quote(locator.slice(tok_range)) { let leading = kind.quote_style();
Some("\"") => Quote::Double,
Some("'") => Quote::Single,
_ => continue,
};
if !contains_escaped_quote(string_contents, leading.opposite().as_char()) { if !contains_escaped_quote(string_contents, leading.opposite().as_char()) {
continue; continue;
} }
@ -333,7 +322,7 @@ pub(crate) fn unnecessary_escaped_quote(
let mut diagnostic = Diagnostic::new(UnnecessaryEscapedQuote, tok_range); let mut diagnostic = Diagnostic::new(UnnecessaryEscapedQuote, tok_range);
let fixed_contents = format!( let fixed_contents = format!(
"{prefix}{quote}{value}{quote}", "{prefix}{quote}{value}{quote}",
prefix = kind.as_str(), prefix = kind.prefix_str(),
quote = leading.as_char(), quote = leading.as_char(),
value = unescape_string(string_contents, leading.opposite().as_char()) value = unescape_string(string_contents, leading.opposite().as_char())
); );
@ -343,16 +332,11 @@ pub(crate) fn unnecessary_escaped_quote(
))); )));
diagnostics.push(diagnostic); diagnostics.push(diagnostic);
} }
Tok::FStringStart => { Tok::FStringStart(kind) => {
let text = locator.slice(tok_range);
// Check for escaped quote only if we're using the preferred quotation // Check for escaped quote only if we're using the preferred quotation
// style and it isn't a triple-quoted f-string. // style and it isn't a triple-quoted f-string.
let check_for_escaped_quote = !is_triple_quote(text); let check_for_escaped_quote = !kind.is_triple_quoted();
let quote_style = if contains_quote(text, Quote::Single.as_char()) { let quote_style = Quote::from(kind.quote_style());
Quote::Single
} else {
Quote::Double
};
fstrings.push(FStringContext::new( fstrings.push(FStringContext::new(
check_for_escaped_quote, check_for_escaped_quote,
tok_range, tok_range,
@ -361,9 +345,8 @@ pub(crate) fn unnecessary_escaped_quote(
} }
Tok::FStringMiddle { Tok::FStringMiddle {
value: string_contents, value: string_contents,
is_raw, kind,
triple_quoted: _, } if !kind.is_raw_string() => {
} if !is_raw => {
let Some(context) = fstrings.last_mut() else { let Some(context) = fstrings.last_mut() else {
continue; continue;
}; };

View file

@ -383,7 +383,7 @@ struct FStringRangeBuilder {
impl FStringRangeBuilder { impl FStringRangeBuilder {
fn visit_token(&mut self, token: &Tok, range: TextRange) { fn visit_token(&mut self, token: &Tok, range: TextRange) {
match token { match token {
Tok::FStringStart => { Tok::FStringStart(_) => {
if self.nesting == 0 { if self.nesting == 0 {
self.start_location = range.start(); self.start_location = range.start();
} }

View file

@ -22,6 +22,15 @@ impl Default for Quote {
} }
} }
impl From<ruff_python_parser::QuoteStyle> for Quote {
fn from(value: ruff_python_parser::QuoteStyle) -> Self {
match value {
ruff_python_parser::QuoteStyle::Double => Self::Double,
ruff_python_parser::QuoteStyle::Single => Self::Single,
}
}
}
#[derive(Debug, CacheKey)] #[derive(Debug, CacheKey)]
pub struct Settings { pub struct Settings {
pub inline_quotes: Quote, pub inline_quotes: Quote,

View file

@ -326,8 +326,10 @@ singles_escaped_unnecessary.py:43:26: Q004 [*] Unnecessary escape on inner quote
| |
41 | # Make sure we do not unescape quotes 41 | # Make sure we do not unescape quotes
42 | this_is_fine = "This is an \\'escaped\\' quote" 42 | this_is_fine = "This is an \\'escaped\\' quote"
43 | this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" 43 | this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004
44 |
45 | # Invalid escapes in bytestrings are also triggered:
| |
= help: Remove backslash = help: Remove backslash
@ -335,7 +337,23 @@ singles_escaped_unnecessary.py:43:26: Q004 [*] Unnecessary escape on inner quote
40 40 | 40 40 |
41 41 | # Make sure we do not unescape quotes 41 41 | # Make sure we do not unescape quotes
42 42 | this_is_fine = "This is an \\'escaped\\' quote" 42 42 | this_is_fine = "This is an \\'escaped\\' quote"
43 |-this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" 43 |-this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
43 |+this_should_raise_Q004 = "This is an \\'escaped\\' quote with an extra backslash" 43 |+this_should_raise_Q004 = "This is an \\'escaped\\' quote with an extra backslash" # Q004
44 44 |
45 45 | # Invalid escapes in bytestrings are also triggered:
46 46 | x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004
singles_escaped_unnecessary.py:46:5: Q004 [*] Unnecessary escape on inner quote character
|
45 | # Invalid escapes in bytestrings are also triggered:
46 | x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004
|
= help: Remove backslash
Safe fix
43 43 | this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
44 44 |
45 45 | # Invalid escapes in bytestrings are also triggered:
46 |-x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004
46 |+x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce'rm\x0e\xcd\xe9.\xf8\xd2" # Q004

View file

@ -3,7 +3,7 @@ use memchr::memchr_iter;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::{StringKind, Tok}; use ruff_python_parser::Tok;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@ -66,21 +66,21 @@ pub(crate) fn invalid_escape_sequence(
token: &Tok, token: &Tok,
token_range: TextRange, token_range: TextRange,
) { ) {
let (token_source_code, string_start_location) = match token { let (token_source_code, string_start_location, kind) = match token {
Tok::FStringMiddle { value, is_raw, .. } => { Tok::FStringMiddle { value, kind } => {
if *is_raw { if kind.is_raw_string() {
return; return;
} }
let Some(range) = indexer.fstring_ranges().innermost(token_range.start()) else { let Some(range) = indexer.fstring_ranges().innermost(token_range.start()) else {
return; return;
}; };
(&**value, range.start()) (&**value, range.start(), kind)
} }
Tok::String { kind, .. } => { Tok::String { kind, .. } => {
if kind.is_raw() { if kind.is_raw_string() {
return; return;
} }
(locator.slice(token_range), token_range.start()) (locator.slice(token_range), token_range.start(), kind)
} }
_ => return, _ => return,
}; };
@ -207,13 +207,7 @@ pub(crate) fn invalid_escape_sequence(
invalid_escape_char.range(), invalid_escape_char.range(),
); );
if matches!( if kind.is_u_string() {
token,
Tok::String {
kind: StringKind::Unicode,
..
}
) {
// Replace the Unicode prefix with `r`. // Replace the Unicode prefix with `r`.
diagnostic.set_fix(Fix::safe_edit(Edit::replacement( diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
"r".to_string(), "r".to_string(),

View file

@ -2,7 +2,6 @@ use std::str::FromStr;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_ast::Expr; use ruff_python_ast::Expr;
use ruff_python_literal::{ use ruff_python_literal::{
cformat::{CFormatErrorType, CFormatString}, cformat::{CFormatErrorType, CFormatString},
@ -10,7 +9,7 @@ use ruff_python_literal::{
format::FromTemplate, format::FromTemplate,
format::{FormatSpec, FormatSpecError, FormatString}, format::{FormatSpec, FormatSpecError, FormatString},
}; };
use ruff_python_parser::{lexer, Mode}; use ruff_python_parser::{lexer, Mode, StringKind, Tok};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -93,15 +92,15 @@ pub(crate) fn call(checker: &mut Checker, string: &str, range: TextRange) {
/// Ex) `"%z" % "1"` /// Ex) `"%z" % "1"`
pub(crate) fn percent(checker: &mut Checker, expr: &Expr) { pub(crate) fn percent(checker: &mut Checker, expr: &Expr) {
// Grab each string segment (in case there's an implicit concatenation). // Grab each string segment (in case there's an implicit concatenation).
let mut strings: Vec<TextRange> = vec![]; let mut strings: Vec<(TextRange, StringKind)> = vec![];
for (tok, range) in for (tok, range) in
lexer::lex_starts_at(checker.locator().slice(expr), Mode::Module, expr.start()).flatten() lexer::lex_starts_at(checker.locator().slice(expr), Mode::Module, expr.start()).flatten()
{ {
if tok.is_string() { match tok {
strings.push(range); Tok::String { kind, .. } => strings.push((range, kind)),
} else if tok.is_percent() {
// Break as soon as we find the modulo symbol. // Break as soon as we find the modulo symbol.
break; Tok::Percent => break,
_ => {}
} }
} }
@ -110,12 +109,10 @@ pub(crate) fn percent(checker: &mut Checker, expr: &Expr) {
return; return;
} }
for range in &strings { for (range, kind) in &strings {
let string = checker.locator().slice(*range); let string = checker.locator().slice(*range);
let (Some(leader), Some(trailer)) = (leading_quote(string), trailing_quote(string)) else { let string = &string
return; [usize::from(kind.opener_len())..(string.len() - usize::from(kind.closer_len()))];
};
let string = &string[leader.len()..string.len() - trailer.len()];
// Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`. // Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`.
if let Err(format_error) = CFormatString::from_str(string) { if let Err(format_error) = CFormatString::from_str(string) {

View file

@ -2,13 +2,12 @@ use std::str::FromStr;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_literal::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString}; use ruff_python_literal::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString};
use ruff_python_parser::{lexer, AsMode}; use ruff_python_parser::{lexer, AsMode, StringKind, Tok};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -219,15 +218,15 @@ fn is_valid_dict(
pub(crate) fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right: &Expr) { pub(crate) fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right: &Expr) {
// Grab each string segment (in case there's an implicit concatenation). // Grab each string segment (in case there's an implicit concatenation).
let content = checker.locator().slice(expr); let content = checker.locator().slice(expr);
let mut strings: Vec<TextRange> = vec![]; let mut strings: Vec<(TextRange, StringKind)> = vec![];
for (tok, range) in for (tok, range) in
lexer::lex_starts_at(content, checker.source_type.as_mode(), expr.start()).flatten() lexer::lex_starts_at(content, checker.source_type.as_mode(), expr.start()).flatten()
{ {
if tok.is_string() { match tok {
strings.push(range); Tok::String { kind, .. } => strings.push((range, kind)),
} else if tok.is_percent() {
// Break as soon as we find the modulo symbol. // Break as soon as we find the modulo symbol.
break; Tok::Percent => break,
_ => {}
} }
} }
@ -238,12 +237,11 @@ pub(crate) fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right:
// Parse each string segment. // Parse each string segment.
let mut format_strings = vec![]; let mut format_strings = vec![];
for range in &strings { for (range, flags) in &strings {
let string = checker.locator().slice(*range); let string = checker.locator().slice(*range);
let (Some(leader), Some(trailer)) = (leading_quote(string), trailing_quote(string)) else { let quote_len = usize::from(flags.quote_len());
return; let string =
}; &string[(usize::from(flags.prefix_len()) + quote_len)..(string.len() - quote_len)];
let string = &string[leader.len()..string.len() - trailer.len()];
// Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`. // Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`.
if let Ok(format_string) = CFormatString::from_str(string) { if let Ok(format_string) = CFormatString::from_str(string) {

View file

@ -3,14 +3,13 @@ use std::str::FromStr;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_ast::whitespace::indentation; use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_literal::cformat::{ use ruff_python_literal::cformat::{
CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString, CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString,
}; };
use ruff_python_parser::{lexer, AsMode, Tok}; use ruff_python_parser::{lexer, AsMode, StringKind, Tok};
use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_stdlib::identifiers::is_identifier;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -353,7 +352,7 @@ fn convertible(format_string: &CFormatString, params: &Expr) -> bool {
/// UP031 /// UP031
pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right: &Expr) { pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right: &Expr) {
// Grab each string segment (in case there's an implicit concatenation). // Grab each string segment (in case there's an implicit concatenation).
let mut strings: Vec<TextRange> = vec![]; let mut strings: Vec<(TextRange, StringKind)> = vec![];
let mut extension = None; let mut extension = None;
for (tok, range) in lexer::lex_starts_at( for (tok, range) in lexer::lex_starts_at(
checker.locator().slice(expr), checker.locator().slice(expr),
@ -362,14 +361,13 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right
) )
.flatten() .flatten()
{ {
if tok.is_string() { match tok {
strings.push(range); Tok::String { kind, .. } => strings.push((range, kind)),
} else if matches!(tok, Tok::Rpar) {
// If we hit a right paren, we have to preserve it. // If we hit a right paren, we have to preserve it.
extension = Some(range); Tok::Rpar => extension = Some(range),
} else if matches!(tok, Tok::Percent) {
// Break as soon as we find the modulo symbol. // Break as soon as we find the modulo symbol.
break; Tok::Percent => break,
_ => continue,
} }
} }
@ -382,12 +380,10 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right
let mut num_positional_arguments = 0; let mut num_positional_arguments = 0;
let mut num_keyword_arguments = 0; let mut num_keyword_arguments = 0;
let mut format_strings = Vec::with_capacity(strings.len()); let mut format_strings = Vec::with_capacity(strings.len());
for range in &strings { for (range, kind) in &strings {
let string = checker.locator().slice(*range); let string = checker.locator().slice(*range);
let (Some(leader), Some(trailer)) = (leading_quote(string), trailing_quote(string)) else { let string = &string
return; [usize::from(kind.opener_len())..(string.len() - usize::from(kind.closer_len()))];
};
let string = &string[leader.len()..string.len() - trailer.len()];
// Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`. // Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`.
let Ok(format_string) = CFormatString::from_str(string) else { let Ok(format_string) = CFormatString::from_str(string) else {
@ -410,8 +406,7 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right
} }
// Convert the `%`-format string to a `.format` string. // Convert the `%`-format string to a `.format` string.
let format_string = percent_to_format(&format_string); format_strings.push(kind.format_string_contents(&percent_to_format(&format_string)));
format_strings.push(format!("{leader}{format_string}{trailer}"));
} }
// Parse the parameters. // Parse the parameters.
@ -459,7 +454,7 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right
// Reconstruct the string. // Reconstruct the string.
let mut contents = String::new(); let mut contents = String::new();
let mut prev = None; let mut prev = None;
for (range, format_string) in strings.iter().zip(format_strings) { for ((range, _), format_string) in strings.iter().zip(format_strings) {
// Add the content before the string segment. // Add the content before the string segment.
match prev { match prev {
None => { None => {

View file

@ -9,7 +9,6 @@ use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok; use ruff_python_parser::Tok;
use ruff_source_file::{find_newline, LineEnding}; use ruff_source_file::{find_newline, LineEnding};
use ruff_python_ast::str::leading_quote;
use ruff_source_file::Locator; use ruff_source_file::Locator;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -44,37 +43,22 @@ impl<'a> Stylist<'a> {
Self { Self {
locator, locator,
indentation, indentation,
quote: detect_quote(tokens, locator), quote: detect_quote(tokens),
line_ending: OnceCell::default(), line_ending: OnceCell::default(),
} }
} }
} }
fn detect_quote(tokens: &[LexResult], locator: &Locator) -> Quote { fn detect_quote(tokens: &[LexResult]) -> Quote {
let quote_range = tokens.iter().flatten().find_map(|(t, range)| match t { for (token, _) in tokens.iter().flatten() {
Tok::String { match token {
triple_quoted: false, Tok::String { kind, .. } if !kind.is_triple_quoted() => {
.. return kind.quote_style().into()
} => Some(*range), }
// No need to check if it's triple-quoted as f-strings cannot be used Tok::FStringStart(kind) => return kind.quote_style().into(),
// as docstrings. _ => continue,
Tok::FStringStart => Some(*range),
_ => None,
});
if let Some(quote_range) = quote_range {
let content = &locator.slice(quote_range);
if let Some(quotes) = leading_quote(content) {
return if quotes.contains('\'') {
Quote::Single
} else if quotes.contains('"') {
Quote::Double
} else {
unreachable!("Expected string to start with a valid quote prefix")
};
} }
} }
Quote::default() Quote::default()
} }
@ -118,6 +102,15 @@ pub enum Quote {
Double, Double,
} }
impl From<ruff_python_parser::QuoteStyle> for Quote {
fn from(value: ruff_python_parser::QuoteStyle) -> Self {
match value {
ruff_python_parser::QuoteStyle::Double => Self::Double,
ruff_python_parser::QuoteStyle::Single => Self::Single,
}
}
}
impl From<Quote> for char { impl From<Quote> for char {
fn from(val: Quote) -> Self { fn from(val: Quote) -> Self {
match val { match val {

View file

@ -87,7 +87,7 @@ pub(crate) struct FStringRangesBuilder {
impl FStringRangesBuilder { impl FStringRangesBuilder {
pub(crate) fn visit_token(&mut self, token: &Tok, range: TextRange) { pub(crate) fn visit_token(&mut self, token: &Tok, range: TextRange) {
match token { match token {
Tok::FStringStart => { Tok::FStringStart(_) => {
self.start_locations.push(range.start()); self.start_locations.push(range.start());
} }
Tok::FStringEnd => { Tok::FStringEnd => {

View file

@ -46,9 +46,8 @@ pub(crate) struct MultilineRangesBuilder {
impl MultilineRangesBuilder { impl MultilineRangesBuilder {
pub(crate) fn visit_token(&mut self, token: &Tok, range: TextRange) { pub(crate) fn visit_token(&mut self, token: &Tok, range: TextRange) {
if let Tok::String { triple_quoted, .. } | Tok::FStringMiddle { triple_quoted, .. } = token if let Tok::String { kind, .. } | Tok::FStringMiddle { kind, .. } = token {
{ if kind.is_triple_quoted() {
if *triple_quoted {
self.ranges.push(range); self.ranges.push(range);
} }
} }

View file

@ -12,7 +12,7 @@
//! # Example //! # Example
//! //!
//! ``` //! ```
//! use ruff_python_parser::{lexer::lex, Tok, Mode, StringKind}; //! use ruff_python_parser::{lexer::lex, Tok, Mode};
//! //!
//! let source = "x = 'RustPython'"; //! let source = "x = 'RustPython'";
//! let tokens = lex(source, Mode::Module) //! let tokens = lex(source, Mode::Module)
@ -37,12 +37,13 @@ use ruff_python_ast::{Int, IpyEscapeKind};
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::lexer::cursor::{Cursor, EOF_CHAR}; use crate::lexer::cursor::{Cursor, EOF_CHAR};
use crate::lexer::fstring::{FStringContext, FStringContextFlags, FStrings}; use crate::lexer::fstring::{FStringContext, FStrings};
use crate::lexer::indentation::{Indentation, Indentations}; use crate::lexer::indentation::{Indentation, Indentations};
use crate::{ use crate::{
soft_keywords::SoftKeywordTransformer, soft_keywords::SoftKeywordTransformer,
string::FStringErrorType, string::FStringErrorType,
token::{StringKind, Tok}, string_token_flags::{StringKind, StringPrefix},
token::Tok,
Mode, Mode,
}; };
@ -181,16 +182,16 @@ impl<'source> Lexer<'source> {
return Ok(self.lex_fstring_start(quote, true)); return Ok(self.lex_fstring_start(quote, true));
} }
(_, quote @ ('\'' | '"')) => { (_, quote @ ('\'' | '"')) => {
if let Ok(string_kind) = StringKind::try_from(first) { if let Ok(prefix) = StringPrefix::try_from(first) {
self.cursor.bump(); self.cursor.bump();
return self.lex_string(string_kind, quote); return self.lex_string(Some(prefix), quote);
} }
} }
(_, second @ ('r' | 'R' | 'b' | 'B')) if is_quote(self.cursor.second()) => { (_, second @ ('r' | 'R' | 'b' | 'B')) if is_quote(self.cursor.second()) => {
self.cursor.bump(); self.cursor.bump();
if let Ok(string_kind) = StringKind::try_from([first, second]) { if let Ok(prefix) = StringPrefix::try_from([first, second]) {
let quote = self.cursor.bump().unwrap(); let quote = self.cursor.bump().unwrap();
return self.lex_string(string_kind, quote); return self.lex_string(Some(prefix), quote);
} }
} }
_ => {} _ => {}
@ -538,19 +539,21 @@ impl<'source> Lexer<'source> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
debug_assert_eq!(self.cursor.previous(), quote); debug_assert_eq!(self.cursor.previous(), quote);
let mut flags = FStringContextFlags::empty(); let mut kind = StringKind::from_prefix(Some(if is_raw_string {
StringPrefix::RawFormat
} else {
StringPrefix::Format
}));
if quote == '"' { if quote == '"' {
flags |= FStringContextFlags::DOUBLE; kind = kind.with_double_quotes();
}
if is_raw_string {
flags |= FStringContextFlags::RAW;
} }
if self.cursor.eat_char2(quote, quote) { if self.cursor.eat_char2(quote, quote) {
flags |= FStringContextFlags::TRIPLE; kind = kind.with_triple_quotes();
} }
self.fstrings.push(FStringContext::new(flags, self.nesting)); self.fstrings.push(FStringContext::new(kind, self.nesting));
Tok::FStringStart Tok::FStringStart(kind)
} }
/// Lex a f-string middle or end token. /// Lex a f-string middle or end token.
@ -683,24 +686,35 @@ impl<'source> Lexer<'source> {
}; };
Ok(Some(Tok::FStringMiddle { Ok(Some(Tok::FStringMiddle {
value: value.into_boxed_str(), value: value.into_boxed_str(),
is_raw: fstring.is_raw_string(), kind: fstring.kind(),
triple_quoted: fstring.is_triple_quoted(),
})) }))
} }
/// Lex a string literal. /// Lex a string literal.
fn lex_string(&mut self, kind: StringKind, quote: char) -> Result<Tok, LexicalError> { fn lex_string(
&mut self,
prefix: Option<StringPrefix>,
quote: char,
) -> Result<Tok, LexicalError> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
debug_assert_eq!(self.cursor.previous(), quote); debug_assert_eq!(self.cursor.previous(), quote);
let mut kind = StringKind::from_prefix(prefix);
if quote == '"' {
kind = kind.with_double_quotes();
}
// If the next two characters are also the quote character, then we have a triple-quoted // If the next two characters are also the quote character, then we have a triple-quoted
// string; consume those two characters and ensure that we require a triple-quote to close // string; consume those two characters and ensure that we require a triple-quote to close
let triple_quoted = self.cursor.eat_char2(quote, quote); if self.cursor.eat_char2(quote, quote) {
kind = kind.with_triple_quotes();
}
let value_start = self.offset(); let value_start = self.offset();
let quote_byte = u8::try_from(quote).expect("char that fits in u8"); let quote_byte = u8::try_from(quote).expect("char that fits in u8");
let value_end = if triple_quoted { let value_end = if kind.is_triple_quoted() {
// For triple-quoted strings, scan until we find the closing quote (ignoring escaped // For triple-quoted strings, scan until we find the closing quote (ignoring escaped
// quotes) or the end of the file. // quotes) or the end of the file.
loop { loop {
@ -712,7 +726,7 @@ impl<'source> Lexer<'source> {
// matches with f-strings quotes and if it is, then this must be a // matches with f-strings quotes and if it is, then this must be a
// missing '}' token so raise the proper error. // missing '}' token so raise the proper error.
if fstring.quote_char() == quote if fstring.quote_char() == quote
&& fstring.is_triple_quoted() == triple_quoted && fstring.is_triple_quoted() == kind.is_triple_quoted()
{ {
return Err(LexicalError::new( return Err(LexicalError::new(
LexicalErrorType::FStringError(FStringErrorType::UnclosedLbrace), LexicalErrorType::FStringError(FStringErrorType::UnclosedLbrace),
@ -761,7 +775,7 @@ impl<'source> Lexer<'source> {
// matches with f-strings quotes and if it is, then this must be a // matches with f-strings quotes and if it is, then this must be a
// missing '}' token so raise the proper error. // missing '}' token so raise the proper error.
if fstring.quote_char() == quote if fstring.quote_char() == quote
&& fstring.is_triple_quoted() == triple_quoted && fstring.is_triple_quoted() == kind.is_triple_quoted()
{ {
return Err(LexicalError::new( return Err(LexicalError::new(
LexicalErrorType::FStringError(FStringErrorType::UnclosedLbrace), LexicalErrorType::FStringError(FStringErrorType::UnclosedLbrace),
@ -832,7 +846,6 @@ impl<'source> Lexer<'source> {
.to_string() .to_string()
.into_boxed_str(), .into_boxed_str(),
kind, kind,
triple_quoted,
}) })
} }
@ -843,7 +856,7 @@ impl<'source> Lexer<'source> {
if !fstring.is_in_expression(self.nesting) { if !fstring.is_in_expression(self.nesting) {
match self.lex_fstring_middle_or_end() { match self.lex_fstring_middle_or_end() {
Ok(Some(tok)) => { Ok(Some(tok)) => {
if tok == Tok::FStringEnd { if tok.is_f_string_end() {
self.fstrings.pop(); self.fstrings.pop();
} }
return Ok((tok, self.token_range())); return Ok((tok, self.token_range()));
@ -1056,7 +1069,7 @@ impl<'source> Lexer<'source> {
c if is_ascii_identifier_start(c) => self.lex_identifier(c)?, c if is_ascii_identifier_start(c) => self.lex_identifier(c)?,
'0'..='9' => self.lex_number(c)?, '0'..='9' => self.lex_number(c)?,
'#' => return Ok((self.lex_comment(), self.token_range())), '#' => return Ok((self.lex_comment(), self.token_range())),
'"' | '\'' => self.lex_string(StringKind::String, c)?, '\'' | '"' => self.lex_string(None, c)?,
'=' => { '=' => {
if self.cursor.eat_char('=') { if self.cursor.eat_char('=') {
Tok::EqEqual Tok::EqEqual

View file

@ -1,27 +1,9 @@
use bitflags::bitflags; use crate::string_token_flags::StringKind;
bitflags! {
#[derive(Debug)]
pub(crate) struct FStringContextFlags: u8 {
/// The current f-string is a triple-quoted f-string i.e., the number of
/// opening quotes is 3. If this flag is not set, the number of opening
/// quotes is 1.
const TRIPLE = 1 << 0;
/// The current f-string is a double-quoted f-string. If this flag is not
/// set, the current f-string is a single-quoted f-string.
const DOUBLE = 1 << 1;
/// The current f-string is a raw f-string i.e., prefixed with `r`/`R`.
/// If this flag is not set, the current f-string is a normal f-string.
const RAW = 1 << 2;
}
}
/// The context representing the current f-string that the lexer is in. /// The context representing the current f-string that the lexer is in.
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct FStringContext { pub(crate) struct FStringContext {
flags: FStringContextFlags, kind: StringKind,
/// The level of nesting for the lexer when it entered the current f-string. /// The level of nesting for the lexer when it entered the current f-string.
/// The nesting level includes all kinds of parentheses i.e., round, square, /// The nesting level includes all kinds of parentheses i.e., round, square,
@ -35,49 +17,47 @@ pub(crate) struct FStringContext {
} }
impl FStringContext { impl FStringContext {
pub(crate) const fn new(flags: FStringContextFlags, nesting: u32) -> Self { pub(crate) const fn new(kind: StringKind, nesting: u32) -> Self {
debug_assert!(kind.is_f_string());
Self { Self {
flags, kind,
nesting, nesting,
format_spec_depth: 0, format_spec_depth: 0,
} }
} }
pub(crate) const fn kind(&self) -> StringKind {
debug_assert!(self.kind.is_f_string());
self.kind
}
pub(crate) const fn nesting(&self) -> u32 { pub(crate) const fn nesting(&self) -> u32 {
self.nesting self.nesting
} }
/// Returns the quote character for the current f-string. /// Returns the quote character for the current f-string.
pub(crate) const fn quote_char(&self) -> char { pub(crate) const fn quote_char(&self) -> char {
if self.flags.contains(FStringContextFlags::DOUBLE) { self.kind.quote_style().as_char()
'"'
} else {
'\''
}
} }
/// Returns the triple quotes for the current f-string if it is a triple-quoted /// Returns the triple quotes for the current f-string if it is a triple-quoted
/// f-string, `None` otherwise. /// f-string, `None` otherwise.
pub(crate) const fn triple_quotes(&self) -> Option<&'static str> { pub(crate) const fn triple_quotes(&self) -> Option<&'static str> {
if self.is_triple_quoted() { if self.is_triple_quoted() {
if self.flags.contains(FStringContextFlags::DOUBLE) { Some(self.kind.quote_str())
Some(r#"""""#)
} else {
Some("'''")
}
} else { } else {
None None
} }
} }
/// Returns `true` if the current f-string is a raw f-string. /// Returns `true` if the current f-string is a raw f-string.
pub(crate) const fn is_raw_string(&self) -> bool { pub(crate) fn is_raw_string(&self) -> bool {
self.flags.contains(FStringContextFlags::RAW) self.kind.is_raw_string()
} }
/// Returns `true` if the current f-string is a triple-quoted f-string. /// Returns `true` if the current f-string is a triple-quoted f-string.
pub(crate) const fn is_triple_quoted(&self) -> bool { pub(crate) const fn is_triple_quoted(&self) -> bool {
self.flags.contains(FStringContextFlags::TRIPLE) self.kind.is_triple_quoted()
} }
/// Calculates the number of open parentheses for the current f-string /// Calculates the number of open parentheses for the current f-string

View file

@ -115,7 +115,8 @@ pub use parser::{
}; };
use ruff_python_ast::{Mod, PySourceType, Suite}; use ruff_python_ast::{Mod, PySourceType, Suite};
pub use string::FStringErrorType; pub use string::FStringErrorType;
pub use token::{StringKind, Tok, TokenKind}; pub use string_token_flags::{QuoteStyle, StringKind};
pub use token::{Tok, TokenKind};
use crate::lexer::LexResult; use crate::lexer::LexResult;
@ -127,6 +128,7 @@ pub mod lexer;
mod parser; mod parser;
mod soft_keywords; mod soft_keywords;
mod string; mod string;
mod string_token_flags;
mod token; mod token;
mod token_source; mod token_source;
pub mod typing; pub mod typing;

View file

@ -12,7 +12,8 @@ use crate::{
function::{ArgumentList, parse_arguments, validate_pos_params, validate_arguments}, function::{ArgumentList, parse_arguments, validate_pos_params, validate_arguments},
context::set_context, context::set_context,
string::{StringType, concatenated_strings, parse_fstring_literal_element, parse_string_literal}, string::{StringType, concatenated_strings, parse_fstring_literal_element, parse_string_literal},
token::{self, StringKind}, string_token_flags::StringKind,
token,
invalid, invalid,
}; };
use lalrpop_util::ParseError; use lalrpop_util::ParseError;
@ -1619,8 +1620,8 @@ StringLiteralOrFString: StringType = {
StringLiteral: StringType = { StringLiteral: StringType = {
<location:@L> <string:string> <end_location:@R> =>? { <location:@L> <string:string> <end_location:@R> =>? {
let (source, kind, triple_quoted) = string; let (source, kind) = string;
Ok(parse_string_literal(source, kind, triple_quoted, (location..end_location).into())?) Ok(parse_string_literal(source, kind, (location..end_location).into())?)
} }
}; };
@ -1636,8 +1637,8 @@ FStringExpr: StringType = {
FStringMiddlePattern: ast::FStringElement = { FStringMiddlePattern: ast::FStringElement = {
FStringReplacementField, FStringReplacementField,
<location:@L> <fstring_middle:fstring_middle> <end_location:@R> =>? { <location:@L> <fstring_middle:fstring_middle> <end_location:@R> =>? {
let (source, is_raw, _) = fstring_middle; let (source, kind) = fstring_middle;
Ok(parse_fstring_literal_element(source, is_raw, (location..end_location).into())?) Ok(parse_fstring_literal_element(source, kind, (location..end_location).into())?)
} }
}; };
@ -2001,7 +2002,7 @@ extern {
Dedent => token::Tok::Dedent, Dedent => token::Tok::Dedent,
StartModule => token::Tok::StartModule, StartModule => token::Tok::StartModule,
StartExpression => token::Tok::StartExpression, StartExpression => token::Tok::StartExpression,
FStringStart => token::Tok::FStringStart, FStringStart => token::Tok::FStringStart(StringKind),
FStringEnd => token::Tok::FStringEnd, FStringEnd => token::Tok::FStringEnd,
"!" => token::Tok::Exclamation, "!" => token::Tok::Exclamation,
"?" => token::Tok::Question, "?" => token::Tok::Question,
@ -2095,12 +2096,10 @@ extern {
string => token::Tok::String { string => token::Tok::String {
value: <Box<str>>, value: <Box<str>>,
kind: <StringKind>, kind: <StringKind>,
triple_quoted: <bool>
}, },
fstring_middle => token::Tok::FStringMiddle { fstring_middle => token::Tok::FStringMiddle {
value: <Box<str>>, value: <Box<str>>,
is_raw: <bool>, kind: <StringKind>,
triple_quoted: <bool>
}, },
name => token::Tok::Name { name: <Box<str>> }, name => token::Tok::Name { name: <Box<str>> },
ipy_escape_command => token::Tok::IpyEscapeCommand { ipy_escape_command => token::Tok::IpyEscapeCommand {

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,13 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
@ -14,13 +20,22 @@ expression: lex_source(source)
( (
String { String {
value: "", value: "",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Double,
},
}, },
4..6, 4..6,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
7..9, 7..9,
), ),
( (
@ -28,7 +43,13 @@ expression: lex_source(source)
9..10, 9..10,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
11..13, 11..13,
), ),
( (
@ -38,13 +59,22 @@ expression: lex_source(source)
( (
String { String {
value: "", value: "",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
15..17, 15..17,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: true,
quote_style: Double,
},
),
18..22, 18..22,
), ),
( (
@ -52,7 +82,13 @@ expression: lex_source(source)
22..25, 22..25,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: true,
quote_style: Single,
},
),
26..30, 26..30,
), ),
( (

View file

@ -6,8 +6,11 @@ expression: lex_source(source)
( (
String { String {
value: "\\N{EN SPACE}", value: "\\N{EN SPACE}",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Double,
},
}, },
0..14, 0..14,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "normal ", value: "normal ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
2..9, 2..9,
), ),
@ -32,8 +41,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " {another} ", value: " {another} ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
14..27, 14..27,
), ),
@ -54,8 +66,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " {", value: " {",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
32..35, 32..35,
), ),
@ -76,8 +91,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "}", value: "}",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
42..44, 42..44,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: true,
quote_style: Double,
},
),
0..4, 0..4,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\n# not a comment ", value: "\n# not a comment ",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Double,
},
}, },
4..21, 4..21,
), ),
@ -46,8 +55,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " # not a comment\n", value: " # not a comment\n",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Double,
},
}, },
42..59, 42..59,
), ),

View file

@ -4,7 +4,13 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
@ -34,8 +40,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
7..8, 7..8,
), ),
@ -70,8 +79,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
14..15, 14..15,
), ),
@ -92,8 +104,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: ".3f!r", value: ".3f!r",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
18..23, 18..23,
), ),
@ -104,8 +119,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " {x!r}", value: " {x!r}",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
24..32, 24..32,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\", value: "\\",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
2..3, 2..3,
), ),
@ -32,8 +41,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "\\\"\\", value: "\\\"\\",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
6..9, 6..9,
), ),
@ -58,8 +70,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " \\\"\\\"\\\n end", value: " \\\"\\\"\\\n end",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
13..24, 13..24,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\", value: "\\",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
2..3, 2..3,
), ),
@ -34,14 +43,23 @@ expression: lex_source(source)
8..9, 8..9,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
10..12, 10..12,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\\\", value: "\\\\",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
12..14, 12..14,
), ),
@ -64,14 +82,23 @@ expression: lex_source(source)
19..20, 19..20,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
21..23, 21..23,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\{foo}", value: "\\{foo}",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
23..31, 23..31,
), ),
@ -80,14 +107,23 @@ expression: lex_source(source)
31..32, 31..32,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
33..35, 33..35,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\\\{foo}", value: "\\\\{foo}",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
35..44, 35..44,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
0..3, 0..3,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\", value: "\\",
is_raw: true, kind: StringKind {
triple_quoted: false, prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
}, },
3..4, 3..4,
), ),
@ -32,8 +41,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "\\\"\\", value: "\\\"\\",
is_raw: true, kind: StringKind {
triple_quoted: false, prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
}, },
7..10, 7..10,
), ),
@ -58,8 +70,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " \\\"\\\"\\\n end", value: " \\\"\\\"\\\n end",
is_raw: true, kind: StringKind {
triple_quoted: false, prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
}, },
14..25, 14..25,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "first ", value: "first ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
2..8, 2..8,
), ),
@ -58,8 +67,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " second", value: " second",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
41..48, 41..48,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: true,
quote_style: Double,
},
),
0..4, 0..4,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\nhello\n world\n", value: "\nhello\n world\n",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Double,
},
}, },
4..21, 4..21,
), ),
@ -20,14 +29,23 @@ expression: lex_source(source)
21..24, 21..24,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: true,
quote_style: Single,
},
),
25..29, 25..29,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\n world\nhello\n", value: "\n world\nhello\n",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Single,
},
}, },
29..46, 29..46,
), ),
@ -36,14 +54,23 @@ expression: lex_source(source)
46..49, 46..49,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
50..52, 50..52,
), ),
( (
FStringMiddle { FStringMiddle {
value: "some ", value: "some ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
52..57, 52..57,
), ),
@ -52,14 +79,23 @@ expression: lex_source(source)
57..58, 57..58,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: true,
quote_style: Double,
},
),
58..62, 58..62,
), ),
( (
FStringMiddle { FStringMiddle {
value: "multiline\nallowed ", value: "multiline\nallowed ",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Double,
},
}, },
62..80, 62..80,
), ),
@ -88,8 +124,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " string", value: " string",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
87..94, 87..94,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\N{BULLET} normal \\Nope \\N", value: "\\N{BULLET} normal \\Nope \\N",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
2..28, 2..28,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
0..3, 0..3,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\N", value: "\\N",
is_raw: true, kind: StringKind {
triple_quoted: false, prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
}, },
3..5, 3..5,
), ),
@ -32,8 +41,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " normal", value: " normal",
is_raw: true, kind: StringKind {
triple_quoted: false, prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
}, },
13..20, 13..20,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "foo ", value: "foo ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
2..6, 2..6,
), ),
@ -20,14 +29,23 @@ expression: lex_source(source)
6..7, 6..7,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
7..9, 7..9,
), ),
( (
FStringMiddle { FStringMiddle {
value: "bar ", value: "bar ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
9..13, 9..13,
), ),
@ -46,7 +64,13 @@ expression: lex_source(source)
16..17, 16..17,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
18..20, 18..20,
), ),
( (
@ -82,8 +106,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " baz", value: " baz",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
29..33, 29..33,
), ),
@ -92,14 +119,23 @@ expression: lex_source(source)
33..34, 33..34,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
35..37, 35..37,
), ),
( (
FStringMiddle { FStringMiddle {
value: "foo ", value: "foo ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
37..41, 37..41,
), ),
@ -108,14 +144,23 @@ expression: lex_source(source)
41..42, 41..42,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
42..44, 42..44,
), ),
( (
FStringMiddle { FStringMiddle {
value: "bar", value: "bar",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
44..47, 44..47,
), ),
@ -130,8 +175,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " some ", value: " some ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
49..55, 49..55,
), ),
@ -140,14 +188,23 @@ expression: lex_source(source)
55..56, 55..56,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
56..58, 56..58,
), ),
( (
FStringMiddle { FStringMiddle {
value: "another", value: "another",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
58..65, 58..65,
), ),

View file

@ -4,7 +4,13 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
@ -20,14 +26,23 @@ expression: lex_source(source)
4..5, 4..5,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
6..8, 6..8,
), ),
( (
FStringMiddle { FStringMiddle {
value: "{}", value: "{}",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
8..12, 8..12,
), ),
@ -36,14 +51,23 @@ expression: lex_source(source)
12..13, 12..13,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
14..16, 14..16,
), ),
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
16..17, 16..17,
), ),
@ -60,14 +84,23 @@ expression: lex_source(source)
19..20, 19..20,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
21..23, 21..23,
), ),
( (
FStringMiddle { FStringMiddle {
value: "{", value: "{",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
23..25, 23..25,
), ),
@ -82,8 +115,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "}", value: "}",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
27..29, 27..29,
), ),
@ -92,14 +128,23 @@ expression: lex_source(source)
29..30, 29..30,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
31..33, 31..33,
), ),
( (
FStringMiddle { FStringMiddle {
value: "{{}}", value: "{{}}",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
33..41, 33..41,
), ),
@ -108,14 +153,23 @@ expression: lex_source(source)
41..42, 41..42,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
43..45, 43..45,
), ),
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
45..46, 45..46,
), ),
@ -130,8 +184,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " {} {", value: " {} {",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
48..56, 48..56,
), ),
@ -146,8 +203,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "} {{}} ", value: "} {{}} ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
58..71, 58..71,
), ),

View file

@ -4,7 +4,13 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
@ -12,7 +18,13 @@ expression: lex_source(source)
2..3, 2..3,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
4..6, 4..6,
), ),
( (
@ -20,7 +32,13 @@ expression: lex_source(source)
6..7, 6..7,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
8..11, 8..11,
), ),
( (
@ -28,7 +46,13 @@ expression: lex_source(source)
11..12, 11..12,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
13..16, 13..16,
), ),
( (
@ -36,7 +60,13 @@ expression: lex_source(source)
16..17, 16..17,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
18..21, 18..21,
), ),
( (
@ -44,7 +74,13 @@ expression: lex_source(source)
21..22, 21..22,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
23..26, 23..26,
), ),
( (
@ -52,7 +88,13 @@ expression: lex_source(source)
26..27, 26..27,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
28..31, 28..31,
), ),
( (
@ -60,7 +102,13 @@ expression: lex_source(source)
31..32, 31..32,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
33..36, 33..36,
), ),
( (
@ -68,7 +116,13 @@ expression: lex_source(source)
36..37, 36..37,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
38..41, 38..41,
), ),
( (
@ -76,7 +130,13 @@ expression: lex_source(source)
41..42, 41..42,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "rf",
triple_quoted: false,
quote_style: Double,
},
),
43..46, 43..46,
), ),
( (

View file

@ -4,14 +4,23 @@ expression: fstring_single_quote_escape_eol(MAC_EOL)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "text \\\r more text", value: "text \\\r more text",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
2..19, 2..19,
), ),

View file

@ -4,14 +4,23 @@ expression: fstring_single_quote_escape_eol(UNIX_EOL)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "text \\\n more text", value: "text \\\n more text",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
2..19, 2..19,
), ),

View file

@ -4,14 +4,23 @@ expression: fstring_single_quote_escape_eol(WINDOWS_EOL)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "text \\\r\n more text", value: "text \\\r\n more text",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
2..20, 2..20,
), ),

View file

@ -4,7 +4,13 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
@ -28,8 +34,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
8..9, 8..9,
), ),
@ -64,8 +73,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: ".3f", value: ".3f",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
15..18, 15..18,
), ),
@ -76,8 +88,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
19..20, 19..20,
), ),
@ -98,8 +113,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: ".", value: ".",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
23..24, 23..24,
), ),
@ -120,8 +138,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "f", value: "f",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
27..28, 27..28,
), ),
@ -132,8 +153,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
29..30, 29..30,
), ),
@ -144,8 +168,11 @@ expression: lex_source(source)
( (
String { String {
value: "", value: "",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
31..33, 31..33,
), ),
@ -156,8 +183,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "*^", value: "*^",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
34..36, 34..36,
), ),
@ -200,8 +230,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
44..45, 44..45,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "foo ", value: "foo ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
2..6, 2..6,
), ),
@ -36,8 +45,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " bar", value: " bar",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
12..16, 12..16,
), ),

View file

@ -4,7 +4,13 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
@ -52,7 +58,13 @@ expression: lex_source(source)
17..18, 17..18,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
18..20, 18..20,
), ),
( (

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: true,
quote_style: Single,
},
),
0..4, 0..4,
), ),
( (
FStringMiddle { FStringMiddle {
value: "__", value: "__",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Single,
},
}, },
4..6, 4..6,
), ),
@ -36,8 +45,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "d\n", value: "d\n",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Single,
},
}, },
14..16, 14..16,
), ),
@ -48,8 +60,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "__", value: "__",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Single,
},
}, },
17..19, 17..19,
), ),
@ -62,14 +77,23 @@ expression: lex_source(source)
22..23, 22..23,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: true,
quote_style: Single,
},
),
23..27, 23..27,
), ),
( (
FStringMiddle { FStringMiddle {
value: "__", value: "__",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Single,
},
}, },
27..29, 27..29,
), ),
@ -94,8 +118,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "a\n b\n c\n", value: "a\n b\n c\n",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Single,
},
}, },
37..61, 37..61,
), ),
@ -106,8 +133,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "__", value: "__",
is_raw: false, kind: StringKind {
triple_quoted: true, prefix: "f",
triple_quoted: true,
quote_style: Single,
},
}, },
62..64, 62..64,
), ),
@ -120,14 +150,23 @@ expression: lex_source(source)
67..68, 67..68,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
68..70, 68..70,
), ),
( (
FStringMiddle { FStringMiddle {
value: "__", value: "__",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
70..72, 70..72,
), ),
@ -152,8 +191,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "d", value: "d",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
80..81, 80..81,
), ),
@ -168,8 +210,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "__", value: "__",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
83..85, 83..85,
), ),
@ -182,14 +227,23 @@ expression: lex_source(source)
86..87, 86..87,
), ),
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
87..89, 87..89,
), ),
( (
FStringMiddle { FStringMiddle {
value: "__", value: "__",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
89..91, 89..91,
), ),
@ -214,8 +268,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "a", value: "a",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
99..100, 99..100,
), ),
@ -240,8 +297,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "__", value: "__",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
112..114, 112..114,
), ),

View file

@ -4,7 +4,13 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Double,
},
),
0..2, 0..2,
), ),
( (
@ -24,8 +30,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: "=10", value: "=10",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
5..8, 5..8,
), ),
@ -36,8 +45,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
9..10, 9..10,
), ),
@ -76,8 +88,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
19..20, 19..20,
), ),
@ -126,8 +141,11 @@ expression: lex_source(source)
( (
FStringMiddle { FStringMiddle {
value: " ", value: " ",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Double,
},
}, },
31..32, 31..32,
), ),

View file

@ -4,14 +4,23 @@ expression: lex_source(source)
--- ---
[ [
( (
FStringStart, FStringStart(
StringKind {
prefix: "f",
triple_quoted: false,
quote_style: Single,
},
),
0..2, 0..2,
), ),
( (
FStringMiddle { FStringMiddle {
value: "\\0", value: "\\0",
is_raw: false, kind: StringKind {
triple_quoted: false, prefix: "f",
triple_quoted: false,
quote_style: Single,
},
}, },
2..4, 2..4,
), ),

View file

@ -14,8 +14,11 @@ expression: lex_source(source)
( (
String { String {
value: "a", value: "a",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
6..9, 6..9,
), ),
@ -26,8 +29,11 @@ expression: lex_source(source)
( (
String { String {
value: "b", value: "b",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
14..17, 14..17,
), ),
@ -42,16 +48,22 @@ expression: lex_source(source)
( (
String { String {
value: "c", value: "c",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
23..26, 23..26,
), ),
( (
String { String {
value: "d", value: "d",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
33..36, 33..36,
), ),

View file

@ -6,72 +6,99 @@ expression: lex_source(source)
( (
String { String {
value: "double", value: "double",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Double,
},
}, },
0..8, 0..8,
), ),
( (
String { String {
value: "single", value: "single",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
9..17, 9..17,
), ),
( (
String { String {
value: "can\\'t", value: "can\\'t",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
18..26, 18..26,
), ),
( (
String { String {
value: "\\\\\\\"", value: "\\\\\\\"",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Double,
},
}, },
27..33, 27..33,
), ),
( (
String { String {
value: "\\t\\r\\n", value: "\\t\\r\\n",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
34..42, 34..42,
), ),
( (
String { String {
value: "\\g", value: "\\g",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
43..47, 43..47,
), ),
( (
String { String {
value: "raw\\'", value: "raw\\'",
kind: RawString, kind: StringKind {
triple_quoted: false, prefix: "r",
triple_quoted: false,
quote_style: Single,
},
}, },
48..56, 48..56,
), ),
( (
String { String {
value: "\\420", value: "\\420",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
57..63, 57..63,
), ),
( (
String { String {
value: "\\200\\0a", value: "\\200\\0a",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Single,
},
}, },
64..73, 64..73,
), ),

View file

@ -6,8 +6,11 @@ expression: string_continuation_with_eol(MAC_EOL)
( (
String { String {
value: "abc\\\rdef", value: "abc\\\rdef",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Double,
},
}, },
0..10, 0..10,
), ),

View file

@ -6,8 +6,11 @@ expression: string_continuation_with_eol(UNIX_EOL)
( (
String { String {
value: "abc\\\ndef", value: "abc\\\ndef",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Double,
},
}, },
0..10, 0..10,
), ),

View file

@ -6,8 +6,11 @@ expression: string_continuation_with_eol(WINDOWS_EOL)
( (
String { String {
value: "abc\\\r\ndef", value: "abc\\\r\ndef",
kind: String, kind: StringKind {
triple_quoted: false, prefix: "",
triple_quoted: false,
quote_style: Double,
},
}, },
0..11, 0..11,
), ),

View file

@ -6,8 +6,11 @@ expression: triple_quoted_eol(MAC_EOL)
( (
String { String {
value: "\r test string\r ", value: "\r test string\r ",
kind: String, kind: StringKind {
triple_quoted: true, prefix: "",
triple_quoted: true,
quote_style: Double,
},
}, },
0..21, 0..21,
), ),

View file

@ -6,8 +6,11 @@ expression: triple_quoted_eol(UNIX_EOL)
( (
String { String {
value: "\n test string\n ", value: "\n test string\n ",
kind: String, kind: StringKind {
triple_quoted: true, prefix: "",
triple_quoted: true,
quote_style: Double,
},
}, },
0..21, 0..21,
), ),

View file

@ -6,8 +6,11 @@ expression: triple_quoted_eol(WINDOWS_EOL)
( (
String { String {
value: "\r\n test string\r\n ", value: "\r\n test string\r\n ",
kind: String, kind: StringKind {
triple_quoted: true, prefix: "",
triple_quoted: true,
quote_style: Double,
},
}, },
0..23, 0..23,
), ),

View file

@ -6,7 +6,8 @@ use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::lexer::{LexicalError, LexicalErrorType}; use crate::lexer::{LexicalError, LexicalErrorType};
use crate::token::{StringKind, Tok}; use crate::string_token_flags::StringKind;
use crate::token::Tok;
pub(crate) enum StringType { pub(crate) enum StringType {
Str(ast::StringLiteral), Str(ast::StringLiteral),
@ -177,9 +178,9 @@ impl StringParser {
'v' => '\x0b', 'v' => '\x0b',
o @ '0'..='7' => self.parse_octet(o as u8), o @ '0'..='7' => self.parse_octet(o as u8),
'x' => self.parse_unicode_literal(2)?, 'x' => self.parse_unicode_literal(2)?,
'u' if !self.kind.is_any_bytes() => self.parse_unicode_literal(4)?, 'u' if !self.kind.is_byte_string() => self.parse_unicode_literal(4)?,
'U' if !self.kind.is_any_bytes() => self.parse_unicode_literal(8)?, 'U' if !self.kind.is_byte_string() => self.parse_unicode_literal(8)?,
'N' if !self.kind.is_any_bytes() => self.parse_unicode_name()?, 'N' if !self.kind.is_byte_string() => self.parse_unicode_name()?,
// Special cases where the escape sequence is not a single character // Special cases where the escape sequence is not a single character
'\n' => return Ok(None), '\n' => return Ok(None),
'\r' => { '\r' => {
@ -190,7 +191,7 @@ impl StringParser {
return Ok(None); return Ok(None);
} }
_ => { _ => {
if self.kind.is_any_bytes() && !first_char.is_ascii() { if self.kind.is_byte_string() && !first_char.is_ascii() {
return Err(LexicalError::new( return Err(LexicalError::new(
LexicalErrorType::OtherError( LexicalErrorType::OtherError(
"bytes can only contain ASCII literal characters" "bytes can only contain ASCII literal characters"
@ -257,7 +258,7 @@ impl StringParser {
// This is still an invalid escape sequence, but we don't want to // This is still an invalid escape sequence, but we don't want to
// raise a syntax error as is done by the CPython parser. It might // raise a syntax error as is done by the CPython parser. It might
// be supported in the future, refer to point 3: https://peps.python.org/pep-0701/#rejected-ideas // be supported in the future, refer to point 3: https://peps.python.org/pep-0701/#rejected-ideas
b'\\' if !self.kind.is_raw() && self.peek_byte().is_some() => { b'\\' if !self.kind.is_raw_string() && self.peek_byte().is_some() => {
match self.parse_escaped_char()? { match self.parse_escaped_char()? {
None => {} None => {}
Some(EscapedChar::Literal(c)) => value.push(c), Some(EscapedChar::Literal(c)) => value.push(c),
@ -302,7 +303,7 @@ impl StringParser {
)); ));
} }
if self.kind.is_raw() { if self.kind.is_raw_string() {
// For raw strings, no escaping is necessary. // For raw strings, no escaping is necessary.
return Ok(StringType::Bytes(ast::BytesLiteral { return Ok(StringType::Bytes(ast::BytesLiteral {
value: self.source.into_boxed_bytes(), value: self.source.into_boxed_bytes(),
@ -355,11 +356,11 @@ impl StringParser {
} }
fn parse_string(mut self) -> Result<StringType, LexicalError> { fn parse_string(mut self) -> Result<StringType, LexicalError> {
if self.kind.is_raw() { if self.kind.is_raw_string() {
// For raw strings, no escaping is necessary. // For raw strings, no escaping is necessary.
return Ok(StringType::Str(ast::StringLiteral { return Ok(StringType::Str(ast::StringLiteral {
value: self.source, value: self.source,
unicode: self.kind.is_unicode(), unicode: self.kind.is_u_string(),
range: self.range, range: self.range,
})); }));
} }
@ -368,7 +369,7 @@ impl StringParser {
// If the string doesn't contain any escape sequences, return the owned string. // If the string doesn't contain any escape sequences, return the owned string.
return Ok(StringType::Str(ast::StringLiteral { return Ok(StringType::Str(ast::StringLiteral {
value: self.source, value: self.source,
unicode: self.kind.is_unicode(), unicode: self.kind.is_u_string(),
range: self.range, range: self.range,
})); }));
}; };
@ -405,13 +406,13 @@ impl StringParser {
Ok(StringType::Str(ast::StringLiteral { Ok(StringType::Str(ast::StringLiteral {
value: value.into_boxed_str(), value: value.into_boxed_str(),
unicode: self.kind.is_unicode(), unicode: self.kind.is_u_string(),
range: self.range, range: self.range,
})) }))
} }
fn parse(self) -> Result<StringType, LexicalError> { fn parse(self) -> Result<StringType, LexicalError> {
if self.kind.is_any_bytes() { if self.kind.is_byte_string() {
self.parse_bytes() self.parse_bytes()
} else { } else {
self.parse_string() self.parse_string()
@ -422,29 +423,16 @@ impl StringParser {
pub(crate) fn parse_string_literal( pub(crate) fn parse_string_literal(
source: Box<str>, source: Box<str>,
kind: StringKind, kind: StringKind,
triple_quoted: bool,
range: TextRange, range: TextRange,
) -> Result<StringType, LexicalError> { ) -> Result<StringType, LexicalError> {
let start_location = range.start() StringParser::new(source, kind, range.start() + kind.opener_len(), range).parse()
+ kind.prefix_len()
+ if triple_quoted {
TextSize::from(3)
} else {
TextSize::from(1)
};
StringParser::new(source, kind, start_location, range).parse()
} }
pub(crate) fn parse_fstring_literal_element( pub(crate) fn parse_fstring_literal_element(
source: Box<str>, source: Box<str>,
is_raw: bool, kind: StringKind,
range: TextRange, range: TextRange,
) -> Result<ast::FStringElement, LexicalError> { ) -> Result<ast::FStringElement, LexicalError> {
let kind = if is_raw {
StringKind::RawString
} else {
StringKind::String
};
StringParser::new(source, kind, range.start(), range).parse_fstring_middle() StringParser::new(source, kind, range.start(), range).parse_fstring_middle()
} }

View file

@ -0,0 +1,314 @@
use std::fmt;
use bitflags::bitflags;
use ruff_text_size::{TextLen, TextSize};
bitflags! {
/// Flags that can be queried to obtain information
/// regarding the prefixes and quotes used for a string literal.
///
/// Note that not all of these flags can be validly combined -- e.g.,
/// it is invalid to combine the `U_PREFIX` flag with any other
/// of the `*_PREFIX` flags. As such, the recommended way to set the
/// prefix flags is by calling the `as_flags()` method on the
/// `StringPrefix` enum.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
struct StringFlags: u8 {
/// The string uses double quotes (`"`).
/// If this flag is not set, the string uses single quotes (`'`).
const DOUBLE = 1 << 0;
/// The string is triple-quoted:
/// it begins and ends with three consecutive quote characters.
const TRIPLE_QUOTED = 1 << 1;
/// The string has a `u` or `U` prefix.
/// While this prefix is a no-op at runtime,
/// strings with this prefix can have no other prefixes set.
const U_PREFIX = 1 << 2;
/// The string has a `b` or `B` prefix.
/// This means that the string is a sequence of `int`s at runtime,
/// rather than a sequence of `str`s.
/// Strings with this flag can also be raw strings,
/// but can have no other prefixes.
const B_PREFIX = 1 << 3;
/// The string has a `f` or `F` prefix, meaning it is an f-string.
/// F-strings can also be raw strings,
/// but can have no other prefixes.
const F_PREFIX = 1 << 4;
/// The string has an `r` or `R` prefix, meaning it is a raw string.
/// F-strings and byte-strings can be raw,
/// as can strings with no other prefixes.
/// U-strings cannot be raw.
const R_PREFIX = 1 << 5;
}
}
/// Enumeration of all the possible valid prefixes
/// prior to a Python string literal.
///
/// Using the `as_flags()` method on variants of this enum
/// is the recommended way to set `*_PREFIX` flags from the
/// `StringFlags` bitflag, as it means that you cannot accidentally
/// set a combination of `*_PREFIX` flags that would be invalid
/// at runtime in Python.
///
/// [String and Bytes literals]: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
/// [PEP 701]: https://peps.python.org/pep-0701/
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(crate) enum StringPrefix {
/// The string has a `u` or `U` prefix.
/// While this prefix is a no-op at runtime,
/// strings with this prefix can have no other prefixes set.
Unicode,
/// The string has an `r` or `R` prefix, meaning it is a raw string.
/// F-strings and byte-strings can be raw,
/// as can strings with no other prefixes.
/// U-strings cannot be raw.
Raw,
/// The string has a `f` or `F` prefix, meaning it is an f-string.
/// F-strings can also be raw strings,
/// but can have no other prefixes.
Format,
/// The string has a `b` or `B` prefix.
/// This means that the string is a sequence of `int`s at runtime,
/// rather than a sequence of `str`s.
/// Bytestrings can also be raw strings,
/// but can have no other prefixes.
Bytes,
/// A string that has has any one of the prefixes
/// `{"rf", "rF", "Rf", "RF", "fr", "fR", "Fr", "FR"}`
/// Semantically, these all have the same meaning:
/// the string is both an f-string and a raw-string
RawFormat,
/// A string that has has any one of the prefixes
/// `{"rb", "rB", "Rb", "RB", "br", "bR", "Br", "BR"}`
/// Semantically, these all have the same meaning:
/// the string is both an bytestring and a raw-string
RawBytes,
}
impl TryFrom<char> for StringPrefix {
type Error = String;
fn try_from(value: char) -> Result<Self, String> {
let result = match value {
'r' | 'R' => Self::Raw,
'u' | 'U' => Self::Unicode,
'b' | 'B' => Self::Bytes,
'f' | 'F' => Self::Format,
_ => return Err(format!("Unexpected prefix '{value}'")),
};
Ok(result)
}
}
impl TryFrom<[char; 2]> for StringPrefix {
type Error = String;
fn try_from(value: [char; 2]) -> Result<Self, String> {
match value {
['r' | 'R', 'f' | 'F'] | ['f' | 'F', 'r' | 'R'] => Ok(Self::RawFormat),
['r' | 'R', 'b' | 'B'] | ['b' | 'B', 'r' | 'R'] => Ok(Self::RawBytes),
_ => Err(format!("Unexpected prefix '{}{}'", value[0], value[1])),
}
}
}
impl StringPrefix {
const fn as_flags(self) -> StringFlags {
match self {
Self::Bytes => StringFlags::B_PREFIX,
Self::Format => StringFlags::F_PREFIX,
Self::Raw => StringFlags::R_PREFIX,
Self::RawBytes => StringFlags::R_PREFIX.union(StringFlags::B_PREFIX),
Self::RawFormat => StringFlags::R_PREFIX.union(StringFlags::F_PREFIX),
Self::Unicode => StringFlags::U_PREFIX,
}
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StringKind(StringFlags);
impl StringKind {
pub(crate) const fn from_prefix(prefix: Option<StringPrefix>) -> Self {
if let Some(prefix) = prefix {
Self(prefix.as_flags())
} else {
Self(StringFlags::empty())
}
}
/// Does the string have a `u` or `U` prefix?
pub const fn is_u_string(self) -> bool {
self.0.contains(StringFlags::U_PREFIX)
}
/// Does the string have an `r` or `R` prefix?
pub const fn is_raw_string(self) -> bool {
self.0.contains(StringFlags::R_PREFIX)
}
/// Does the string have an `f` or `F` prefix?
pub const fn is_f_string(self) -> bool {
self.0.contains(StringFlags::F_PREFIX)
}
/// Does the string have a `b` or `B` prefix?
pub const fn is_byte_string(self) -> bool {
self.0.contains(StringFlags::B_PREFIX)
}
/// Does the string use single or double quotes in its opener and closer?
pub const fn quote_style(self) -> QuoteStyle {
if self.0.contains(StringFlags::DOUBLE) {
QuoteStyle::Double
} else {
QuoteStyle::Single
}
}
/// Is the string triple-quoted, i.e.,
/// does it begin and end with three consecutive quote characters?
pub const fn is_triple_quoted(self) -> bool {
self.0.contains(StringFlags::TRIPLE_QUOTED)
}
/// A `str` representation of the quotes used to start and close.
/// This does not include any prefixes the string has in its opener.
pub const fn quote_str(self) -> &'static str {
if self.is_triple_quoted() {
match self.quote_style() {
QuoteStyle::Single => "'''",
QuoteStyle::Double => r#"""""#,
}
} else {
match self.quote_style() {
QuoteStyle::Single => "'",
QuoteStyle::Double => "\"",
}
}
}
/// A `str` representation of the prefixes used (if any)
/// in the string's opener.
pub const fn prefix_str(self) -> &'static str {
if self.0.contains(StringFlags::F_PREFIX) {
if self.0.contains(StringFlags::R_PREFIX) {
return "rf";
}
return "f";
}
if self.0.contains(StringFlags::B_PREFIX) {
if self.0.contains(StringFlags::R_PREFIX) {
return "rb";
}
return "b";
}
if self.0.contains(StringFlags::R_PREFIX) {
return "r";
}
if self.0.contains(StringFlags::U_PREFIX) {
return "u";
}
""
}
/// The length of the prefixes used (if any) in the string's opener.
pub fn prefix_len(self) -> TextSize {
self.prefix_str().text_len()
}
/// The length of the quotes used to start and close the string.
/// This does not include the length of any prefixes the string has
/// in its opener.
pub const fn quote_len(self) -> TextSize {
if self.is_triple_quoted() {
TextSize::new(3)
} else {
TextSize::new(1)
}
}
/// The total length of the string's opener,
/// i.e., the length of the prefixes plus the length
/// of the quotes used to open the string.
pub fn opener_len(self) -> TextSize {
self.prefix_len() + self.quote_len()
}
/// The total length of the string's closer.
/// This is always equal to `self.quote_len()`,
/// but is provided here for symmetry with the `opener_len()` method.
pub const fn closer_len(self) -> TextSize {
self.quote_len()
}
pub fn format_string_contents(self, contents: &str) -> String {
format!(
"{}{}{}{}",
self.prefix_str(),
self.quote_str(),
contents,
self.quote_str()
)
}
#[must_use]
pub fn with_double_quotes(mut self) -> Self {
self.0 |= StringFlags::DOUBLE;
self
}
#[must_use]
pub fn with_triple_quotes(mut self) -> Self {
self.0 |= StringFlags::TRIPLE_QUOTED;
self
}
}
impl fmt::Debug for StringKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StringKind")
.field("prefix", &self.prefix_str())
.field("triple_quoted", &self.is_triple_quoted())
.field("quote_style", &self.quote_style())
.finish()
}
}
#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
pub enum QuoteStyle {
/// E.g. '
Single,
/// E.g. "
#[default]
Double,
}
impl QuoteStyle {
pub const fn as_char(self) -> char {
match self {
Self::Single => '\'',
Self::Double => '"',
}
}
#[must_use]
pub const fn opposite(self) -> Self {
match self {
Self::Single => Self::Double,
Self::Double => Self::Single,
}
}
}

View file

@ -4,10 +4,10 @@
//! loosely based on the token definitions found in the [CPython source]. //! loosely based on the token definitions found in the [CPython source].
//! //!
//! [CPython source]: https://github.com/python/cpython/blob/dfc2e065a2e71011017077e549cd2f9bf4944c54/Include/internal/pycore_token.h; //! [CPython source]: https://github.com/python/cpython/blob/dfc2e065a2e71011017077e549cd2f9bf4944c54/Include/internal/pycore_token.h;
use crate::string_token_flags::StringKind;
use crate::Mode; use crate::Mode;
use ruff_python_ast::{Int, IpyEscapeKind}; use ruff_python_ast::{Int, IpyEscapeKind};
use ruff_text_size::TextSize;
use std::fmt; use std::fmt;
/// The set of tokens the Python source code can be tokenized in. /// The set of tokens the Python source code can be tokenized in.
@ -39,23 +39,21 @@ pub enum Tok {
String { String {
/// The string value. /// The string value.
value: Box<str>, value: Box<str>,
/// The kind of string. /// Flags that can be queried to determine the quote style
/// and prefixes of the string
kind: StringKind, kind: StringKind,
/// Whether the string is triple quoted.
triple_quoted: bool,
}, },
/// Token value for the start of an f-string. This includes the `f`/`F`/`fr` prefix /// Token value for the start of an f-string. This includes the `f`/`F`/`fr` prefix
/// and the opening quote(s). /// and the opening quote(s).
FStringStart, FStringStart(StringKind),
/// Token value that includes the portion of text inside the f-string that's not /// Token value that includes the portion of text inside the f-string that's not
/// part of the expression part and isn't an opening or closing brace. /// part of the expression part and isn't an opening or closing brace.
FStringMiddle { FStringMiddle {
/// The string value. /// The string value.
value: Box<str>, value: Box<str>,
/// Whether the string is raw or not. /// Flags that can be queried to determine the quote style
is_raw: bool, /// and prefixes of the string
/// Whether the string is triple quoted. kind: StringKind,
triple_quoted: bool,
}, },
/// Token value for the end of an f-string. This includes the closing quote. /// Token value for the end of an f-string. This includes the closing quote.
FStringEnd, FStringEnd,
@ -243,15 +241,10 @@ impl fmt::Display for Tok {
Int { value } => write!(f, "'{value}'"), Int { value } => write!(f, "'{value}'"),
Float { value } => write!(f, "'{value}'"), Float { value } => write!(f, "'{value}'"),
Complex { real, imag } => write!(f, "{real}j{imag}"), Complex { real, imag } => write!(f, "{real}j{imag}"),
String { String { value, kind } => {
value, write!(f, "{}", kind.format_string_contents(value))
kind,
triple_quoted,
} => {
let quotes = "\"".repeat(if *triple_quoted { 3 } else { 1 });
write!(f, "{kind}{quotes}{value}{quotes}")
} }
FStringStart => f.write_str("FStringStart"), FStringStart(_) => f.write_str("FStringStart"),
FStringMiddle { value, .. } => f.write_str(value), FStringMiddle { value, .. } => f.write_str(value),
FStringEnd => f.write_str("FStringEnd"), FStringEnd => f.write_str("FStringEnd"),
IpyEscapeCommand { kind, value } => write!(f, "{kind}{value}"), IpyEscapeCommand { kind, value } => write!(f, "{kind}{value}"),
@ -354,103 +347,6 @@ impl fmt::Display for Tok {
} }
} }
/// The kind of string literal as described in the [String and Bytes literals]
/// section of the Python reference.
///
/// Note that f-strings are not included here, because as of [PEP 701] they
/// emit different tokens than other string literals.
///
/// [String and Bytes literals]: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
/// [PEP 701]: https://peps.python.org/pep-0701/
#[derive(PartialEq, Eq, Debug, Clone, Hash, Copy)] // TODO: is_macro::Is
pub enum StringKind {
/// A normal string literal with no prefix.
String,
/// A byte string literal, with a `b` or `B` prefix.
Bytes,
/// A raw string literal, with a `r` or `R` prefix.
RawString,
/// A raw byte string literal, with a `rb`/`br` or `rB`/`Br` or `Rb`/`bR` or `RB`/`BR` prefix.
RawBytes,
/// A unicode string literal, with a `u` or `U` prefix.
Unicode,
}
impl TryFrom<char> for StringKind {
type Error = String;
fn try_from(ch: char) -> Result<Self, String> {
match ch {
'r' | 'R' => Ok(StringKind::RawString),
'u' | 'U' => Ok(StringKind::Unicode),
'b' | 'B' => Ok(StringKind::Bytes),
c => Err(format!("Unexpected string prefix: {c}")),
}
}
}
impl TryFrom<[char; 2]> for StringKind {
type Error = String;
fn try_from(chars: [char; 2]) -> Result<Self, String> {
match chars {
['r' | 'R', 'b' | 'B'] => Ok(StringKind::RawBytes),
['b' | 'B', 'r' | 'R'] => Ok(StringKind::RawBytes),
[c1, c2] => Err(format!("Unexpected string prefix: {c1}{c2}")),
}
}
}
impl fmt::Display for StringKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl StringKind {
/// Returns true if the string is a raw string, i,e one of
/// [`StringKind::RawString`] or [`StringKind::RawBytes`].
pub fn is_raw(&self) -> bool {
use StringKind::{RawBytes, RawString};
matches!(self, RawString | RawBytes)
}
/// Returns true if the string is a byte string, i,e one of
/// [`StringKind::Bytes`] or [`StringKind::RawBytes`].
pub fn is_any_bytes(&self) -> bool {
use StringKind::{Bytes, RawBytes};
matches!(self, Bytes | RawBytes)
}
/// Returns true if the string is a unicode string, i,e [`StringKind::Unicode`].
pub fn is_unicode(&self) -> bool {
matches!(self, StringKind::Unicode)
}
/// Returns the number of characters in the prefix.
pub fn prefix_len(&self) -> TextSize {
use StringKind::{Bytes, RawBytes, RawString, String, Unicode};
let len = match self {
String => 0,
RawString | Unicode | Bytes => 1,
RawBytes => 2,
};
len.into()
}
pub fn as_str(&self) -> &'static str {
use StringKind::{Bytes, RawBytes, RawString, String, Unicode};
match self {
String => "",
Bytes => "b",
RawString => "r",
RawBytes => "rb",
Unicode => "u",
}
}
}
// TODO move to ruff_python_parser?
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub enum TokenKind { pub enum TokenKind {
/// Token value for a name, commonly known as an identifier. /// Token value for a name, commonly known as an identifier.
@ -804,7 +700,7 @@ impl TokenKind {
Tok::Float { .. } => TokenKind::Float, Tok::Float { .. } => TokenKind::Float,
Tok::Complex { .. } => TokenKind::Complex, Tok::Complex { .. } => TokenKind::Complex,
Tok::String { .. } => TokenKind::String, Tok::String { .. } => TokenKind::String,
Tok::FStringStart => TokenKind::FStringStart, Tok::FStringStart(_) => TokenKind::FStringStart,
Tok::FStringMiddle { .. } => TokenKind::FStringMiddle, Tok::FStringMiddle { .. } => TokenKind::FStringMiddle,
Tok::FStringEnd => TokenKind::FStringEnd, Tok::FStringEnd => TokenKind::FStringEnd,
Tok::IpyEscapeCommand { .. } => TokenKind::EscapeCommand, Tok::IpyEscapeCommand { .. } => TokenKind::EscapeCommand,