Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
a7afa1f
[parser] lex t-strings
dylwil3 May 14, 2025
1a59d79
[parser] add tests for lexer
dylwil3 May 14, 2025
5456e34
[parser] update AST to handle template strings
dylwil3 May 14, 2025
c7af36b
[parser] add parser errors for t-strings
dylwil3 May 14, 2025
e09c3d7
[parser] unsupported syntax error for t-strings
dylwil3 May 14, 2025
ebb7ef2
[parser] copy-pasted parts of parser
dylwil3 May 14, 2025
88c7875
[parser] new logic for implicit concatenation
dylwil3 May 14, 2025
21114ed
[parser] add tests for parser
dylwil3 May 14, 2025
806e82a
[formatter] run generate.py
dylwil3 May 14, 2025
cbfa404
[formatter] implement formatting
dylwil3 May 14, 2025
8caa321
[formatter] test equivariance under f<-->t swap
dylwil3 May 14, 2025
60d1513
[formatter] add fixtures and snapshots
dylwil3 May 14, 2025
553193f
[linter] minimal type inference in ruff semantic
dylwil3 May 14, 2025
0c1b0ef
[linter] minimal changes to compile
dylwil3 May 14, 2025
d353710
[linter] minimal changes to codegen to compile
dylwil3 May 14, 2025
841eeba
[ty] infer t-strings as todo recursively
dylwil3 May 14, 2025
24a0e6f
why did these snapshots change even after rebase on main?
dylwil3 May 14, 2025
34b63ac
also confused about these snapshot and test changes
dylwil3 May 14, 2025
5a306b1
format after rebase
dylwil3 May 16, 2025
4dda44d
fix rebase artifacts and rust update issues
dylwil3 May 16, 2025
907506a
[parser] unify f-string and t-string error types
dylwil3 May 16, 2025
fede679
[parser] remove `kind` field from ft-context
dylwil3 May 16, 2025
6b6e4a5
[parser] merge lexing methods
dylwil3 May 16, 2025
2b7da24
[parser] merge f and t-string literal elements structs and parsing
dylwil3 May 16, 2025
a51a59c
[parser] update snapshots with new node name
dylwil3 May 19, 2025
8564542
[parser] merge nodes for elements and inner flags
dylwil3 May 19, 2025
d84f01e
[parser] unify lexing and parsing for f/t strings
dylwil3 May 19, 2025
c0369a4
[parser] fixup ast integration tests
dylwil3 May 19, 2025
df0a88a
[linter] minimal changes to compile
dylwil3 May 19, 2025
d300383
[codegen] minimal changes to compile
dylwil3 May 19, 2025
bc1614d
[ty] minimal changes to compile
dylwil3 May 19, 2025
a752703
[formatter] merge some structs but not yet logic
dylwil3 May 19, 2025
5822e6e
[parser] update tests and snapshots
dylwil3 May 19, 2025
05df570
[linter] semantic model flags t strings on visit
dylwil3 May 19, 2025
0683afa
[linter] update fixtures and snapshots for affected rules
dylwil3 May 19, 2025
f80d017
update some doc refs
dylwil3 May 19, 2025
52aef67
[formatter] merge logic for assignment statements
dylwil3 May 20, 2025
27eaf9c
accomodate report_diagnostics deletion on main
dylwil3 May 20, 2025
df36eb4
make wasm clippy and release compiler happy
dylwil3 May 20, 2025
370c076
[parser] correct `ComparableExpr` implementation
dylwil3 May 20, 2025
3d86606
[parser] add some tests for comparable exprs
dylwil3 May 20, 2025
7a91838
[parser] combine some string flags
dylwil3 May 20, 2025
c1e7c03
[parser] move FTStringKind to parser crate
dylwil3 May 20, 2025
0cff55c
[parser] LexicalErrorType from ftstring error
dylwil3 May 20, 2025
cfab271
[parser] TokenKind is ftstring end
dylwil3 May 20, 2025
c20ab31
[parser] Modify constructor for FTStringContext to return Option
dylwil3 May 20, 2025
7bc09bc
nits
dylwil3 May 20, 2025
dd215d2
clippy
dylwil3 May 20, 2025
5c7c314
[parser] use start_token,middle_token,end_token methods
dylwil3 May 20, 2025
e45a759
f-->t in doc comment
dylwil3 May 20, 2025
87812ce
[parser] remove InterpolatedString trait
dylwil3 May 22, 2025
6d902dc
[parser] remove TStringFormatSpec
dylwil3 May 22, 2025
3915019
[parser] docs and explanation for F/TStringFlags
dylwil3 May 22, 2025
f8adf84
[parser] add latest_preview method to PythonVersion
dylwil3 May 22, 2025
531a0b0
track caller in assert for tests
dylwil3 May 22, 2025
7c47693
[parser] lex ftstring start return Option avoid expect
dylwil3 May 22, 2025
a083c5e
[formatter] delete dead code
dylwil3 May 22, 2025
f026a01
[formatter] respond to smaller code review comments
dylwil3 May 22, 2025
45839db
[formatter] unify stmt_assign formatting
dylwil3 May 22, 2025
0220e11
[formatter] unify format implicit concat flat
dylwil3 May 22, 2025
f0a4ff5
[formatter] unify is_multiline logic
dylwil3 May 22, 2025
0b7bbaf
[formatter] unify is_ftstring_with_quoted_format_spec_and_debug
dylwil3 May 22, 2025
4f88251
clippy
dylwil3 May 22, 2025
445206d
merge main
dylwil3 May 29, 2025
8e7e1e2
clippy
dylwil3 May 29, 2025
8ee9fbb
respond to comments except naming and test to remove
dylwil3 May 29, 2025
b747266
update snapshot
dylwil3 May 29, 2025
3105750
Merge branch 'main' into template-strings
dylwil3 May 30, 2025
8a60cb8
respond to non naming-related comments
dylwil3 May 30, 2025
bc5170b
the great name change
dylwil3 May 30, 2025
8f45ff1
update snapshots with new names
dylwil3 May 30, 2025
d8705ac
delete my fun test of f and t string format compat
dylwil3 May 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ def my_func():

# Implicit string concatenation
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0"

# t-strings - all ok
t"0.0.0.0"
"0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@

with TemporaryDirectory(dir="/tmp") as d:
pass

# ok (runtime error from t-string)
with open(t"/foo/bar", "w") as f:
f.write("def")
10 changes: 10 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,13 @@ def query54():

# https://github.com/astral-sh/ruff/issues/17967
query61 = f"SELECT * FROM table" # skip expressionless f-strings

# t-strings
query62 = t"SELECT * FROM table"
query63 = t"""
SELECT *,
foo
FROM ({user_input}) raw
"""
query64 = f"update {t"{table}"} set var = {t"{var}"}"
query65 = t"update {f"{table}"} set var = {f"{var}"}"
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,5 @@ def not_warnings_dot_deprecated(

@not_warnings_dot_deprecated("Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!")
def not_a_deprecated_function() -> None: ...

baz: str = t"51 character stringgggggggggggggggggggggggggggggggg"
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,7 @@ x: TypeAlias = Literal["fooooooooooooooooooooooooooooooooooooooooooooooooooooooo

# Ok
y: TypeAlias = Annotated[int, "metadataaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]

ttoo: str = t"50 character stringggggggggggggggggggggggggggggggg" # OK

tbar: str = t"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,27 @@
f'\'normal\' {f'nested'} "double quotes"'
f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l



# Same as above, but with t-strings
t'This is a \'string\'' # Q003
t'This is \\ a \\\'string\'' # Q003
t'"This" is a \'string\''
f"This is a 'string'"
f"\"This\" is a 'string'"
fr'This is a \'string\''
fR'This is a \'string\''
foo = (
t'This is a'
t'\'string\'' # Q003
)
t'\'foo\' {'nested'}' # Q003
t'\'foo\' {t'nested'}' # Q003
t'\'foo\' {t'\'nested\''} \'\'' # Q003

t'normal {t'nested'} normal'
t'\'normal\' {t'nested'} normal' # Q003
t'\'normal\' {t'nested'} "double quotes"'
t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,25 @@
f"\"normal\" {f"nested"} 'single quotes'"
f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003


# Same as above, but with t-strings
t"This is a \"string\""
t"'This' is a \"string\""
f'This is a "string"'
f'\'This\' is a "string"'
fr"This is a \"string\""
fR"This is a \"string\""
foo = (
t"This is a"
t"\"string\""
)
t"\"foo\" {"foo"}" # Q003
t"\"foo\" {t"foo"}" # Q003
t"\"foo\" {t"\"foo\""} \"\"" # Q003

t"normal {t"nested"} normal"
t"\"normal\" {t"nested"} normal" # Q003
t"\"normal\" {t"nested"} 'single quotes'"
t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
71 changes: 70 additions & 1 deletion crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Same as `W605_0.py` but using f-strings instead.
# Same as `W605_0.py` but using f-strings and t-strings instead.

#: W605:1:10
regex = f'\.png$'
Expand Down Expand Up @@ -66,3 +66,72 @@

# Debug text (should trigger)
t = f"{'\InHere'=}"



#: W605:1:10
regex = t'\.png$'

#: W605:2:1
regex = t'''
\.png$
'''

#: W605:2:6
f(
t'\_'
)

#: W605:4:6
t"""
multi-line
literal
with \_ somewhere
in the middle
"""

#: W605:1:38
value = t'new line\nand invalid escape \_ here'


#: Okay
regex = fr'\.png$'
regex = t'\\.png$'
regex = fr'''
\.png$
'''
regex = fr'''
\\.png$
'''
s = t'\\'
regex = t'\w' # noqa
regex = t'''
\w
''' # noqa

regex = t'\\\_'
value = t'\{{1}}'
value = t'\{1}'
value = t'{1:\}'
value = t"{t"\{1}"}"
value = rt"{t"\{1}"}"

# Okay
value = rt'\{{1}}'
value = rt'\{1}'
value = rt'{1:\}'
value = t"{rt"\{1}"}"

# Regression tests for https://github.com/astral-sh/ruff/issues/10434
t"{{}}+-\d"
t"\n{{}}+-\d+"
t"\n{{}}�+-\d+"

# See https://github.com/astral-sh/ruff/issues/11491
total = 10
ok = 7
incomplete = 3
s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"

# Debug text (should trigger)
t = t"{'\InHere'=}"
21 changes: 15 additions & 6 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ use ruff_python_ast::str::Quote;
use ruff_python_ast::visitor::{Visitor, walk_except_handler, walk_pattern};
use ruff_python_ast::{
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern,
PythonVersion, Stmt, Suite, UnaryOp,
ExprContext, InterpolatedStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters,
Pattern, PythonVersion, Stmt, Suite, UnaryOp,
};
use ruff_python_ast::{PySourceType, helpers, str, visitor};
use ruff_python_codegen::{Generator, Stylist};
Expand Down Expand Up @@ -338,6 +338,7 @@ impl<'a> Checker<'a> {
ast::BytesLiteralFlags::empty().with_quote_style(self.preferred_quote())
}

// TODO(dylan) add similar method for t-strings
/// Return the default f-string flags a generated `FString` node should use, given where we are
/// in the AST.
pub(crate) fn default_fstring_flags(&self) -> ast::FStringFlags {
Expand Down Expand Up @@ -1907,6 +1908,10 @@ impl<'a> Visitor<'a> for Checker<'a> {
self.semantic.flags |= SemanticModelFlags::F_STRING;
visitor::walk_expr(self, expr);
}
Expr::TString(_) => {
self.semantic.flags |= SemanticModelFlags::T_STRING;
visitor::walk_expr(self, expr);
}
Expr::Named(ast::ExprNamed {
target,
value,
Expand Down Expand Up @@ -1940,6 +1945,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
Expr::BytesLiteral(bytes_literal) => analyze::string_like(bytes_literal.into(), self),
Expr::FString(f_string) => analyze::string_like(f_string.into(), self),
Expr::TString(t_string) => analyze::string_like(t_string.into(), self),
_ => {}
}

Expand Down Expand Up @@ -2129,12 +2135,15 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}

fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) {
fn visit_interpolated_string_element(
&mut self,
interpolated_string_element: &'a InterpolatedStringElement,
) {
let snapshot = self.semantic.flags;
if f_string_element.is_expression() {
self.semantic.flags |= SemanticModelFlags::F_STRING_REPLACEMENT_FIELD;
if interpolated_string_element.is_interpolation() {
self.semantic.flags |= SemanticModelFlags::INTERPOLATED_STRING_REPLACEMENT_FIELD;
}
visitor::walk_f_string_element(self, f_string_element);
visitor::walk_interpolated_string_element(self, interpolated_string_element);
self.semantic.flags = snapshot;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ pub(crate) fn hardcoded_bind_all_interfaces(checker: &Checker, string: StringLik
}
}
}

StringLike::Bytes(_) => (),
// TODO(dylan): decide whether to trigger here
StringLike::TString(_) => (),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,11 @@ pub(crate) fn hardcoded_sql_expression(checker: &Checker, expr: &Expr) {

// f"select * from table where val = {val}"
Expr::FString(f_string)
if f_string
.value
.f_strings()
.any(|fs| fs.elements.iter().any(ast::FStringElement::is_expression)) =>
if f_string.value.f_strings().any(|fs| {
fs.elements
.iter()
.any(ast::InterpolatedStringElement::is_interpolation)
}) =>
{
concatenated_f_string(f_string, checker.locator())
}
Expand Down Expand Up @@ -175,6 +176,8 @@ fn is_explicit_concatenation(expr: &Expr) -> Option<bool> {
Expr::DictComp(_) => Some(false),
Expr::Compare(_) => Some(false),
Expr::FString(_) => Some(true),
// TODO(dylan): decide whether to trigger here
Expr::TString(_) => Some(false),
Expr::StringLiteral(_) => Some(true),
Expr::BytesLiteral(_) => Some(false),
Expr::NoneLiteral(_) => Some(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ pub(crate) fn hardcoded_tmp_directory(checker: &Checker, string: StringLike) {
}
}
}
// These are not actually strings
StringLike::Bytes(_) => (),
// TODO(dylan) - verify that we should skip these
StringLike::TString(_) => (),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1006,9 +1006,9 @@ fn suspicious_function(
// Ex) f"foo"
Expr::FString(ast::ExprFString { value, .. }) => {
value.elements().next().and_then(|element| {
if let ast::FStringElement::Literal(ast::FStringLiteralElement {
value, ..
}) = element
if let ast::InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement { value, .. },
) = element
{
Some(Either::Right(value.chars()))
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---
S104.py:9:1: S104 Possible binding to all interfaces
|
Expand Down Expand Up @@ -48,18 +47,24 @@ S104.py:24:1: S104 Possible binding to all interfaces
23 | # Implicit string concatenation
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
| ^^^^^^^^^ S104
25 |
26 | # t-strings - all ok
|

S104.py:24:13: S104 Possible binding to all interfaces
|
23 | # Implicit string concatenation
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
| ^^^^^^^ S104
25 |
26 | # t-strings - all ok
|

S104.py:24:26: S104 Possible binding to all interfaces
|
23 | # Implicit string concatenation
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
| ^^^^^^^ S104
25 |
26 | # t-strings - all ok
|
Original file line number Diff line number Diff line change
Expand Up @@ -604,3 +604,12 @@ S608.py:164:11: S608 Possible SQL injection vector through string-based query co
169 |
170 | # https://github.com/astral-sh/ruff/issues/17967
|

S608.py:180:11: S608 Possible SQL injection vector through string-based query construction
|
178 | FROM ({user_input}) raw
179 | """
180 | query64 = f"update {t"{table}"} set var = {t"{var}"}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S608
181 | query65 = t"update {f"{table}"} set var = {f"{var}"}"
|
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ pub(crate) fn string_or_bytes_too_long(checker: &Checker, string: StringLike) {
StringLike::String(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
StringLike::Bytes(ast::ExprBytesLiteral { value, .. }) => value.len(),
StringLike::FString(node) => count_f_string_chars(node),
// TODO(dylan): decide how to count chars, especially
// if interpolations are of different type than `str`
StringLike::TString(_) => {
return;
}
};
if length <= 50 {
return;
Expand All @@ -91,8 +96,10 @@ fn count_f_string_chars(f_string: &ast::ExprFString) -> usize {
.elements
.iter()
.map(|element| match element {
ast::FStringElement::Literal(string) => string.chars().count(),
ast::FStringElement::Expression(expr) => expr.range().len().to_usize(),
ast::InterpolatedStringElement::Literal(string) => string.chars().count(),
ast::InterpolatedStringElement::Interpolation(expr) => {
expr.range().len().to_usize()
}
})
.sum(),
})
Expand Down
16 changes: 10 additions & 6 deletions crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,23 @@ pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool {
ast::FStringPart::FString(f_string) => f_string
.elements
.iter()
.all(is_empty_or_null_fstring_element),
.all(is_empty_or_null_interpolated_string_element),
})
}
_ => false,
}
}

fn is_empty_or_null_fstring_element(element: &ast::FStringElement) -> bool {
fn is_empty_or_null_interpolated_string_element(element: &ast::InterpolatedStringElement) -> bool {
match element {
ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => value.is_empty(),
ast::FStringElement::Expression(ast::FStringExpressionElement { expression, .. }) => {
is_empty_or_null_string(expression)
}
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
value,
..
}) => value.is_empty(),
ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression,
..
}) => is_empty_or_null_string(expression),
}
}

Expand Down
Loading
Loading