Repository: astral-sh/ruff
File: crates/ruff_python_ast/src/helpers.rs (Rust)

Code before:
                            // For tuple(generator), we can't determine statically if the result will
                            // be empty or not, so return Unknown. The generator itself is truthy, but
                            // tuple(empty_generator) is falsy. ListComp and SetComp are handled by
                            // recursing into Self::from_expr below, which returns Unknown for them.
                            if argument.is_generator_expr() {
                                Self::Unknown
                            } else {
                                // Return Unknown for types with definite truthiness that might result
                                // in empty iterables or will raise a type error. Explicitly list types
                                // we recurse for (types without definite truthiness or where Self::from_expr
                                // correctly handles the truthiness).
                                match argument {
                                    // Types that raise TypeError when passed to tuple/list/set
                                    Expr::NumberLiteral(_)
                                    | Expr::BooleanLiteral(_)
                                    | Expr::NoneLiteral(_)
                                    | Expr::EllipsisLiteral(_) => {
                                        // For non-iterable arguments like numbers, booleans, None, etc.,
                                        // we can't assume truthiness. For example, tuple(0) raises TypeError.
                                        Self::Unknown
                                    }
                                    // Types with definite truthiness that might result in empty iterables
                                    Expr::StringLiteral(_)
                                    | Expr::TString(_)
                                    | Expr::FString(_)
                                    | Expr::BytesLiteral(_) => {
                                        // For strings/t-strings/f-strings/bytes, tuple("") creates an empty tuple
                                        // which is falsy, but tuple("a") creates a non-empty tuple which
                                        // is truthy. However, we can't assume truthiness matches because
                                        // we conflate truthiness with non-emptiness. For SIM222/SIM223,
                                        // we should be conservative and return Unknown to avoid false positives.
                                        Self::Unknown
                                    }
                                    // Recurse for collections - we need to check if they're empty
                                    Expr::List(_)
                                    | Expr::Set(_)
                                    | Expr::Tuple(_)
                                    | Expr::Dict(_) => Self::from_expr(argument, is_builtin),
                                    // Recurse for comprehensions - they return Unknown in from_expr
                                    Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) => {
                                        Self::from_expr(argument, is_builtin)
                                    }
                                    // Recurse for variables and other expressions without definite truthiness
                                    Expr::Name(_)
                                    | Expr::Call(_)
                                    | Expr::Attribute(_)
                                    | Expr::Subscript(_)
                                    | Expr::BinOp(_)
                                    | Expr::UnaryOp(_)
                                    | Expr::If(_)
                                    | Expr::Starred(_)

Review comment (suggestion):
I think we can recurse for all of these except t-strings. `StringLiteral`, `BytesLiteral`, and `FString` all check if they are empty or not. Only `TString` maps to `Self::Truthy` unconditionally.

Code after:
                            match argument {
                                // Return Unknown for types with definite truthiness that might
                                // result in empty iterables (t-strings and generators) or will
                                // raise a type error (non-iterable types like numbers, booleans,
                                // None, etc.).
                                Expr::NumberLiteral(_)
                                | Expr::BooleanLiteral(_)
                                | Expr::NoneLiteral(_)
                                | Expr::EllipsisLiteral(_)
                                | Expr::TString(_)
                                | Expr::Lambda(_)
                                | Expr::Generator(_) => Self::Unknown,
                                // Recurse for all other types - collections, comprehensions, variables, etc.
                                // StringLiteral, FString, and BytesLiteral recurse because Self::from_expr
                                // correctly handles their truthiness (checking if empty or not).
                                _ => Self::from_expr(argument, is_builtin),
                            }
                        } else {
                            Self::Unknown
                        }
                    } else {
                        Self::Unknown
                    }
                } else {
                    Self::Unknown
                }
            }
            _ => Self::Unknown,
        }
    }

    pub fn into_bool(self) -> Option<bool> {
        match self {
            Self::True | Self::Truthy => Some(true),
            Self::False | Self::Falsey => Some(false),
            Self::None => Some(false),
            Self::Unknown => None,
        }
    }
}

/// Returns `true` if the expression definitely resolves to a non-empty string, when used as an
/// f-string expression, or `false` if the expression may resolve to an empty string.
fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
    fn inner(expr: &Expr) -> bool {
        match expr {
            // When stringified, these expressions are always non-empty.
            Expr::Lambda(_) => true,
            Expr::Dict(_) => true,
            Expr::Set(_) => true,
            Expr::ListComp(_) => true,