Align t-string/bytes mix error with CPython 3.14 (#27766)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Augustin BouverotandMicha Reiser authored and GitHub committed 2026-08-19 12:35:23 +02:00
1 parent 309c3340c3
commit 3ea20f6161
3 files changed
+499 -15

No files matched your search

@@ -0,0 +1,7 @@
t'first' b'second'
b'first' t'second'
t'first' br'second'
'first' b'second' t'third'
b'first' 'second' t'third'
'first' t'second' 'third' b'fourth'
b'first' t'second' f'third'
@@ -1387,16 +1387,20 @@ impl<'src> Parser<'src> {
// We could convert the node into a string and mark it as invalid
// and would be clever to mark the type which is fewer in quantity.
// test_err mixed_tstring_and_bytes_literals
// t'first' b'second'
// b'first' t'second'
// t'first' br'second'
// 'first' b'second' t'third'
// b'first' 'second' t'third'
// 'first' t'second' 'third' b'fourth'
// b'first' t'second' f'third'
// test_err mixed_bytes_and_non_bytes_literals
// 'first' b'second'
// f'first' b'second'
// 'first' f'second' b'third'
self.add_error(
ParseErrorType::OtherError(
"Bytes literal cannot be mixed with non-bytes literals".to_string(),
),
range,
);
self.report_mixed_string_literal_error(&strings, range);
}
// Only construct a byte expression if all the literals are bytes
// otherwise, we'll try either string, t-string, or f-string. This is to retain
@@ -1415,16 +1419,9 @@ impl<'src> Parser<'src> {
node_index: AtomicNodeIndex::NONE,
});
}
}
if has_tstring {
} else if has_tstring {
if tstring_count < strings.len() {
self.add_error(
ParseErrorType::OtherError(
"Cannot mix t-string literals with string or bytes literals".to_string(),
),
range,
);
self.report_mixed_string_literal_error(&strings, range);
}
// Only construct a t-string expression if all the literals are t-strings
// otherwise, we'll try either string or f-string. This is to retain
@@ -1506,6 +1503,26 @@ impl<'src> Parser<'src> {
})
}
fn report_mixed_string_literal_error(&mut self, strings: &[StringType], range: TextRange) {
// CPython reports the first incompatible pair. A t-string mismatch takes
// precedence over a bytes mismatch within that pair.
for pair in strings.windows(2) {
let message = match pair {
[StringType::TString(_), StringType::TString(_)]
| [StringType::Bytes(_), StringType::Bytes(_)] => continue,
[StringType::TString(_), _] | [_, StringType::TString(_)] => {
"Cannot mix t-string literals with string or bytes literals"
}
[StringType::Bytes(_), _] | [_, StringType::Bytes(_)] => {
"Bytes literal cannot be mixed with non-bytes literals"
}
_ => continue,
};
self.add_error(ParseErrorType::OtherError(message.to_string()), range);
break;
}
}
/// Parses a single string or byte literal.
///
/// This does not handle implicitly concatenated strings.
@@ -0,0 +1,460 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/mixed_tstring_and_bytes_literals.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..176,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 0..18,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 0..18,
value: FStringValue {
inner: Concatenated(
[
Literal(
StringLiteral {
range: 0..8,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 9..18,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
],
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 19..37,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 19..37,
value: FStringValue {
inner: Concatenated(
[
Literal(
StringLiteral {
range: 19..27,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 28..37,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
],
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 38..57,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 38..57,
value: FStringValue {
inner: Concatenated(
[
Literal(
StringLiteral {
range: 38..46,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 47..57,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
],
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 58..84,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 58..84,
value: FStringValue {
inner: Concatenated(
[
Literal(
StringLiteral {
range: 58..65,
node_index: NodeIndex(None),
value: "first",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 66..75,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 76..84,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
],
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 85..111,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 85..111,
value: FStringValue {
inner: Concatenated(
[
Literal(
StringLiteral {
range: 85..93,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 94..102,
node_index: NodeIndex(None),
value: "second",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 103..111,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
],
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 112..147,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 112..147,
value: FStringValue {
inner: Concatenated(
[
Literal(
StringLiteral {
range: 112..119,
node_index: NodeIndex(None),
value: "first",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 120..129,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 130..137,
node_index: NodeIndex(None),
value: "third",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 138..147,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
],
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 148..175,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 148..175,
value: FStringValue {
inner: Concatenated(
[
Literal(
StringLiteral {
range: 148..156,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 157..166,
node_index: NodeIndex(None),
value: "",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
FString(
FString {
range: 167..175,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 169..174,
node_index: NodeIndex(None),
value: "third",
},
),
],
flags: FStringFlags {
quote_style: Single,
prefix: Regular,
triple_quoted: false,
unclosed: false,
},
},
),
],
),
},
},
),
},
),
],
},
)
```
## Errors
|
1 | t'first' b'second'
| ^^^^^^^^^^^^^^^^^^ Syntax Error: Cannot mix t-string literals with string or bytes literals
2 | b'first' t'second'
3 | t'first' br'second'
|
|
1 | t'first' b'second'
2 | b'first' t'second'
| ^^^^^^^^^^^^^^^^^^ Syntax Error: Cannot mix t-string literals with string or bytes literals
3 | t'first' br'second'
4 | 'first' b'second' t'third'
|
|
1 | t'first' b'second'
2 | b'first' t'second'
3 | t'first' br'second'
| ^^^^^^^^^^^^^^^^^^^ Syntax Error: Cannot mix t-string literals with string or bytes literals
4 | 'first' b'second' t'third'
5 | b'first' 'second' t'third'
|
|
2 | b'first' t'second'
3 | t'first' br'second'
4 | 'first' b'second' t'third'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Bytes literal cannot be mixed with non-bytes literals
5 | b'first' 'second' t'third'
6 | 'first' t'second' 'third' b'fourth'
|
|
3 | t'first' br'second'
4 | 'first' b'second' t'third'
5 | b'first' 'second' t'third'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Bytes literal cannot be mixed with non-bytes literals
6 | 'first' t'second' 'third' b'fourth'
7 | b'first' t'second' f'third'
|
|
4 | 'first' b'second' t'third'
5 | b'first' 'second' t'third'
6 | 'first' t'second' 'third' b'fourth'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Cannot mix t-string literals with string or bytes literals
7 | b'first' t'second' f'third'
|
|
5 | b'first' 'second' t'third'
6 | 'first' t'second' 'third' b'fourth'
7 | b'first' t'second' f'third'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Cannot mix t-string literals with string or bytes literals