Repository: astral-sh/ruff
File: crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs (Rust)

Code before:
                ["gettext", "gettext" | "ngettext"] | ["builtins", "_"]
            )
        })
}

/// Return `true` if `call_expr` is a method call on an [`ast::ExprStringLiteral`]
/// in which `literal` is one of the [`ast::StringLiteral`] parts.
///
/// For example: `expr` is a node representing the expression `"{foo}".format(foo="bar")`,
/// and `literal` is the node representing the string literal `"{foo}"`.
fn is_method_call_on_literal(call_expr: &ast::ExprCall, literal: &ast::StringLiteral) -> bool {
    let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = &*call_expr.func else {
        return false;
    };
    let ast::Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &**value else {
        return false;
    };
    value.as_slice().contains(literal)
}

/// Returns `true` if `text` contains a `#` character that would start a comment,
/// i.e., a `#` that appears outside of any nested string literal.
///
/// In Python < 3.12, f-string interpolations cannot contain comments, but a `#`
/// inside a nested string (e.g., `f"{'#'}"`) is not a comment and is valid.
fn has_comment_hash(text: &str) -> bool {
    let mut chars = text.chars().peekable();
    while let Some(ch) = chars.next() {
        match ch {
            // Entering a string literal — skip until the matching closing quote.
            '\'' | '"' => {
                let quote = ch;
                // Check for triple-quote.
                let triple = chars.as_str().starts_with([quote, quote].as_slice())
                    && {
                        // consume the two extra quotes
                        chars.next();
                        chars.next();
                        true
                    };
                loop {
                    match chars.next() {
                        None => break,
                        Some('\\') => {
                            chars.next(); // skip escaped char
                        }
                        Some(c) if c == quote => {
                            if triple {
                                // need two more matching quotes
                                if chars.as_str().starts_with([quote, quote].as_slice()) {
                                    chars.next();

Review comment (suggestion):
I'm a bit wary of duplicating this kind of parsing logic in this rule. Do we have access to the tokens or another way to reuse existing parsing code here?

I'm also curious if this handles cases like:

```py
"{1:#x}"
```

correctly. I tried to test it locally, but the compilation errors prevented me from running the code.

Code after:
                ["gettext", "gettext" | "ngettext"] | ["builtins", "_"]
            )
        })
}

/// Return `true` if `call_expr` is a method call on an [`ast::ExprStringLiteral`]
/// in which `literal` is one of the [`ast::StringLiteral`] parts.
///
/// For example: `expr` is a node representing the expression `"{foo}".format(foo="bar")`,
/// and `literal` is the node representing the string literal `"{foo}"`.
fn is_method_call_on_literal(call_expr: &ast::ExprCall, literal: &ast::StringLiteral) -> bool {
    let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = &*call_expr.func else {
        return false;
    };
    let ast::Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &**value else {
        return false;
    };
    value.as_slice().contains(literal)
}

/// Returns `true` if `literal` is likely an f-string with a missing `f` prefix.
/// See [`MissingFStringSyntax`] for the validation criteria.
fn should_be_fstring(
    literal: &ast::StringLiteral,
    locator: &Locator,
    semantic: &SemanticModel,
    target_version: PythonVersion,
) -> bool {
    if !has_brackets(&literal.value) {
        return false;
    }

    let fstring_expr = format!("f{}", locator.slice(literal));
    let Ok(parsed) = parse_expression(&fstring_expr) else {
        return false;
    };

    // For Python < 3.12, reject if the parser detected any PEP 701 f-string
    // features.
    if target_version < PythonVersion::PY312 {
        let has_pep701 = parsed
            .unsupported_syntax_errors()
            .iter()
            .any(|e| matches!(e.kind, UnsupportedSyntaxErrorKind::Pep701FString(_)));
        if has_pep701 {
            return false;
        }
    }

    // Note: Range offsets for `value` are based on `fstring_expr`
    let ast::Expr::FString(ast::ExprFString { value, .. }) = parsed.expr() else {