[refurb] Fix false positive on empty tuples (FURB168) (#19058)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

This PR fixes #19047 / the [isinstance-type-none
(FURB168)](https://docs.astral.sh/ruff/rules/isinstance-type-none/#isinstance-type-none-furb168)
tuple false positive by adding a check if the tuple is empty to the
code. I also noticed there was another false positive with the other
tuple check in the same function, so I fixed it the same way.
`Union[()]` is invalid at runtime with `TypeError: Cannot take a Union
of no types.`, but it is accepted by `basedpyright`
[playground](https://basedpyright.com/?pythonVersion=3.8&typeCheckingMode=all&code=GYJw9gtgBALgngBwJYDsDmUkQWEMoCqKSYKAsAFAgCmAbtQIYA2A%2BvAtQBREkoDanAJQBdQUA)
and is equivalent to `Never`, so I fixed it anyways. I'm getting on a
side tangent here, but it looks like MyPy doesn't accept it, and ty
[playground](https://play.ty.dev/c2c468b6-38e4-4dd9-a9fa-0276e843e395)
gives `@Todo`.

## Test Plan

<!-- How was it tested? -->

Added two test cases for the two false positives.
[playground](https://play.ruff.rs/a53afc21-9a1d-4b9b-9346-abfbeabeb449)
This commit is contained in:
GiGaGon 2025-07-01 07:26:41 -07:00 committed by GitHub
parent 8cc14ad02d
commit cc736c3a51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 12 additions and 2 deletions

View file

@ -85,3 +85,10 @@ def _():
if isinstance(foo, type(None)):
...
# https://github.com/astral-sh/ruff/issues/19047
if isinstance(foo, ()):
pass
if isinstance(foo, Union[()]):
pass

View file

@ -100,7 +100,9 @@ fn is_none(expr: &Expr, semantic: &SemanticModel) -> bool {
}
// Ex) `(type(None),)`
Expr::Tuple(tuple) => tuple.iter().all(|element| inner(element, false, semantic)),
Expr::Tuple(tuple) => {
!tuple.is_empty() && tuple.iter().all(|element| inner(element, false, semantic))
}
// Ex) `type(None) | type(None)`
Expr::BinOp(ast::ExprBinOp {
@ -125,7 +127,8 @@ fn is_none(expr: &Expr, semantic: &SemanticModel) -> bool {
match slice.as_ref() {
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
elts.iter().all(|element| inner(element, true, semantic))
!elts.is_empty()
&& elts.iter().all(|element| inner(element, true, semantic))
}
slice => inner(slice, true, semantic),
}