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

Code before:
                } else {
                    Self::Truthy
                }
            }
            Expr::Call(ast::ExprCall {
                func, arguments, ..
            }) => {
                if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() {
                    if is_iterable_initializer(id.as_str(), |id| is_builtin(id)) {
                        if arguments.is_empty() {
                            // Ex) `list()`
                            Self::Falsey
                        } else if let [argument] = &*arguments.args
                            && arguments.keywords.is_empty()
                        {
                            // Ex) `list([1, 2, 3])`
                            // 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 if is_definitely_not_iterable(argument) {
                                // For non-iterable arguments like numbers, booleans, None, etc.,
                                // we can't assume truthiness. For example, tuple(0) raises TypeError.
                                Self::Unknown
                            } else if matches!(argument, Expr::StringLiteral(_)) {
                                // For strings, 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:
                                // - tuple("") is falsy (empty tuple), but "" is also falsy
                                // - The issue is that we conflate truthiness with non-emptiness
                                // - For SIM222/SIM223, we should be conservative and return Unknown
                                //   to avoid false positives
                                Self::Unknown
                            } else {
                                Self::from_expr(argument, is_builtin)
                            }
                        } else {
                            Self::Unknown
                        }
                    } else {
                        Self::Unknown
                    }
                } else {
                    Self::Unknown
                }
            }
            _ => Self::Unknown,
        }
    }

Review comment (suggestion):
I wonder if it might make more sense to list the types we explicitly want to recurse for instead of filtering out the cases where we don't want to recurse. Maybe just even a big `match` on `argument` would be easiest to reason about. What do you think?

Basically I think we need to return `Self::Unknown` here and avoid recursing for any type that has a definite `Self::Truthy` or `Self::Falsey` (or `Self::None`) type in one of the match arms above this, if the type might result in an empty iterable or will raise a type error.

As it stands, I don't think this PR actually fixes the t-string case from the issue.

Code after:
                            && arguments.keywords.is_empty()
                        {
                            // Ex) `list([1, 2, 3])`
                            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,