From dc29f52750f7086c3bd0e4495a93ec4acf58edac Mon Sep 17 00:00:00 2001 From: Simon Brugman Date: Thu, 28 Nov 2024 19:07:12 +0100 Subject: [PATCH] [`flake8-pyi`, `ruff`] Fix traversal of nested literals and unions (`PYI016`, `PYI051`, `PYI055`, `PYI062`, `RUF041`) (#14641) --- .../test/fixtures/flake8_pyi/PYI016.py | 3 + .../test/fixtures/flake8_pyi/PYI016.pyi | 5 +- ...__flake8_pyi__tests__PYI016_PYI016.py.snap | 50 ++++++++++++++- ..._flake8_pyi__tests__PYI016_PYI016.pyi.snap | 50 ++++++++++++++- ...__flake8_pyi__tests__PYI041_PYI041.py.snap | 32 ++++++++++ ..._flake8_pyi__tests__PYI041_PYI041.pyi.snap | 28 ++++++++ ...__flake8_pyi__tests__PYI051_PYI051.py.snap | 29 +++++++++ ..._flake8_pyi__tests__PYI051_PYI051.pyi.snap | 29 +++++++++ ...__flake8_pyi__tests__PYI055_PYI055.py.snap | 18 +++--- ..._flake8_pyi__tests__PYI055_PYI055.pyi.snap | 47 +++++++++++++- ...__flake8_pyi__tests__PYI062_PYI062.py.snap | 18 ------ ..._flake8_pyi__tests__PYI062_PYI062.pyi.snap | 18 ------ ..._rules__ruff__tests__RUF041_RUF041.py.snap | 41 ------------ ...rules__ruff__tests__RUF041_RUF041.pyi.snap | 42 ------------ .../src/analyze/typing.rs | 15 +++-- crates/ruff_python_semantic/src/model.rs | 64 +++++++++++-------- 16 files changed, 319 insertions(+), 170 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py index 967f056a923459..a4ff51f5dea672 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py @@ -108,3 +108,6 @@ def func2() -> str | str: # PYI016: Duplicate union member `str` # Test case for mixed union type fix field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + +# Test case for mixed union type +field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi index 7fc398be264084..a4ff51f5dea672 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi @@ -107,4 +107,7 @@ field31: typing.Union[float, typing.Union[int | int]] # Error field32: typing.Union[float, typing.Union[int | int | int]] # Error # Test case for mixed union type fix -field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error \ No newline at end of file +field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + +# Test case for mixed union type +field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap index f293a09c14daf3..b95e7475b48b28 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap @@ -330,6 +330,16 @@ PYI016.py:89:41: PYI016 Duplicate union member `int` | = help: Remove duplicate union member `int` +PYI016.py:92:54: PYI016 Duplicate union member `int` + | +91 | # Should emit in cases with nested `typing.Union` +92 | field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +93 | +94 | # Should emit in cases with mixed `typing.Union` and `|` + | + = help: Remove duplicate union member `int` + PYI016.py:95:29: PYI016 Duplicate union member `int` | 94 | # Should emit in cases with mixed `typing.Union` and `|` @@ -340,6 +350,16 @@ PYI016.py:95:29: PYI016 Duplicate union member `int` | = help: Remove duplicate union member `int` +PYI016.py:98:54: PYI016 Duplicate union member `int` + | + 97 | # Should emit twice in cases with multiple nested `typing.Union` + 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + | ^^^ PYI016 + 99 | +100 | # Should emit once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + PYI016.py:98:59: PYI016 Duplicate union member `int` | 97 | # Should emit twice in cases with multiple nested `typing.Union` @@ -350,6 +370,16 @@ PYI016.py:98:59: PYI016 Duplicate union member `int` | = help: Remove duplicate union member `int` +PYI016.py:101:54: PYI016 Duplicate union member `int` + | +100 | # Should emit once in cases with multiple nested `typing.Union` +101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + | ^^^ PYI016 +102 | +103 | # Should emit once, and fix to `typing.Union[float, int]` + | + = help: Remove duplicate union member `int` + PYI016.py:104:49: PYI016 Duplicate union member `int` | 103 | # Should emit once, and fix to `typing.Union[float, int]` @@ -385,21 +415,35 @@ PYI016.py:110:42: PYI016 Duplicate union member `int` 109 | # Test case for mixed union type fix 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error | ^^^ PYI016 +111 | +112 | # Test case for mixed union type | = help: Remove duplicate union member `int` -PYI016.py:110:49: PYI016 Duplicate union member `typing.Union[int | int]` +PYI016.py:110:62: PYI016 Duplicate union member `int` | 109 | # Test case for mixed union type fix 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error - | ^^^^^^^^^^^^^^^^^^^^^^^ PYI016 + | ^^^ PYI016 +111 | +112 | # Test case for mixed union type | - = help: Remove duplicate union member `typing.Union[int | int]` + = help: Remove duplicate union member `int` PYI016.py:110:68: PYI016 Duplicate union member `int` | 109 | # Test case for mixed union type fix 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error | ^^^ PYI016 +111 | +112 | # Test case for mixed union type | = help: Remove duplicate union member `int` + +PYI016.py:113:61: PYI016 Duplicate union member `list[int]` + | +112 | # Test case for mixed union type +113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + | ^^^^^^^^^ PYI016 + | + = help: Remove duplicate union member `list[int]` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap index 9576706c017c6d..b49b909b044b82 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap @@ -330,6 +330,16 @@ PYI016.pyi:89:41: PYI016 Duplicate union member `int` | = help: Remove duplicate union member `int` +PYI016.pyi:92:54: PYI016 Duplicate union member `int` + | +91 | # Should emit in cases with nested `typing.Union` +92 | field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +93 | +94 | # Should emit in cases with mixed `typing.Union` and `|` + | + = help: Remove duplicate union member `int` + PYI016.pyi:95:29: PYI016 Duplicate union member `int` | 94 | # Should emit in cases with mixed `typing.Union` and `|` @@ -340,6 +350,16 @@ PYI016.pyi:95:29: PYI016 Duplicate union member `int` | = help: Remove duplicate union member `int` +PYI016.pyi:98:54: PYI016 Duplicate union member `int` + | + 97 | # Should emit twice in cases with multiple nested `typing.Union` + 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + | ^^^ PYI016 + 99 | +100 | # Should emit once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + PYI016.pyi:98:59: PYI016 Duplicate union member `int` | 97 | # Should emit twice in cases with multiple nested `typing.Union` @@ -350,6 +370,16 @@ PYI016.pyi:98:59: PYI016 Duplicate union member `int` | = help: Remove duplicate union member `int` +PYI016.pyi:101:54: PYI016 Duplicate union member `int` + | +100 | # Should emit once in cases with multiple nested `typing.Union` +101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + | ^^^ PYI016 +102 | +103 | # Should emit once, and fix to `typing.Union[float, int]` + | + = help: Remove duplicate union member `int` + PYI016.pyi:104:49: PYI016 Duplicate union member `int` | 103 | # Should emit once, and fix to `typing.Union[float, int]` @@ -385,21 +415,35 @@ PYI016.pyi:110:42: PYI016 Duplicate union member `int` 109 | # Test case for mixed union type fix 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error | ^^^ PYI016 +111 | +112 | # Test case for mixed union type | = help: Remove duplicate union member `int` -PYI016.pyi:110:49: PYI016 Duplicate union member `typing.Union[int | int]` +PYI016.pyi:110:62: PYI016 Duplicate union member `int` | 109 | # Test case for mixed union type fix 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error - | ^^^^^^^^^^^^^^^^^^^^^^^ PYI016 + | ^^^ PYI016 +111 | +112 | # Test case for mixed union type | - = help: Remove duplicate union member `typing.Union[int | int]` + = help: Remove duplicate union member `int` PYI016.pyi:110:68: PYI016 Duplicate union member `int` | 109 | # Test case for mixed union type fix 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error | ^^^ PYI016 +111 | +112 | # Test case for mixed union type | = help: Remove duplicate union member `int` + +PYI016.pyi:113:61: PYI016 Duplicate union member `list[int]` + | +112 | # Test case for mixed union type +113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + | ^^^^^^^^^ PYI016 + | + = help: Remove duplicate union member `list[int]` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.py.snap index 6b86c89dc61b9b..f34cdc6b516b1c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.py.snap @@ -25,6 +25,14 @@ PYI041.py:30:28: PYI041 Use `float` instead of `int | float` | = help: Remove redundant type +PYI041.py:34:26: PYI041 Use `float` instead of `int | float` + | +34 | def f3(arg1: int, *args: Union[int | int | float]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +35 | ... + | + = help: Remove redundant type + PYI041.py:38:24: PYI041 Use `float` instead of `int | float` | 38 | async def f4(**kwargs: int | int | float) -> None: @@ -41,6 +49,30 @@ PYI041.py:42:26: PYI041 Use `float` instead of `int | float` | = help: Remove redundant type +PYI041.py:46:26: PYI041 Use `float` instead of `int | float` + | +46 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +47 | ... + | + = help: Remove redundant type + +PYI041.py:50:26: PYI041 Use `float` instead of `int | float` + | +50 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +51 | ... + | + = help: Remove redundant type + +PYI041.py:54:26: PYI041 Use `float` instead of `int | float` + | +54 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +55 | ... + | + = help: Remove redundant type + PYI041.py:59:10: PYI041 Use `complex` instead of `int | float | complex` | 58 | def f9( diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.pyi.snap index 4cbf5870626ceb..dab4f24e281f4d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.pyi.snap @@ -22,6 +22,13 @@ PYI041.pyi:27:28: PYI041 Use `float` instead of `int | float` | = help: Remove redundant type +PYI041.pyi:30:26: PYI041 Use `float` instead of `int | float` + | +30 | def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + PYI041.pyi:33:24: PYI041 Use `float` instead of `int | float` | 33 | async def f4(**kwargs: int | int | float) -> None: ... # PYI041 @@ -66,6 +73,27 @@ PYI041.pyi:49:26: PYI041 Use `float` instead of `int | float` | = help: Remove redundant type +PYI041.pyi:52:26: PYI041 Use `float` instead of `int | float` + | +52 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +PYI041.pyi:55:26: PYI041 Use `float` instead of `int | float` + | +55 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +PYI041.pyi:58:26: PYI041 Use `float` instead of `int | float` + | +58 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + PYI041.pyi:64:24: PYI041 Use `complex` instead of `int | float | complex` | 62 | def good(self, arg: int) -> None: ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI051_PYI051.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI051_PYI051.py.snap index 19b66cf9714801..db8a1c52f7ae6e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI051_PYI051.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI051_PYI051.py.snap @@ -70,6 +70,35 @@ PYI051.py:7:51: PYI051 `Literal[42]` is redundant in a union with `int` 9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] | +PYI051.py:8:76: PYI051 `Literal["foo"]` is redundant in a union with `str` + | + 6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]] + 7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int] + 8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]] + | ^^^^^ PYI051 + 9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] +10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] + | + +PYI051.py:9:81: PYI051 `Literal["foo"]` is redundant in a union with `str` + | + 7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int] + 8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]] + 9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] + | ^^^^^ PYI051 +10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] + | + +PYI051.py:10:69: PYI051 `Literal["foo"]` is redundant in a union with `str` + | + 8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]] + 9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] +10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] + | ^^^^^ PYI051 +11 | +12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ... + | + PYI051.py:12:31: PYI051 `Literal[1J]` is redundant in a union with `complex` | 10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI051_PYI051.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI051_PYI051.pyi.snap index c7fba04db43aaf..8ddcd4893a1158 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI051_PYI051.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI051_PYI051.pyi.snap @@ -70,6 +70,35 @@ PYI051.pyi:7:51: PYI051 `Literal[42]` is redundant in a union with `int` 9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] | +PYI051.pyi:8:76: PYI051 `Literal["foo"]` is redundant in a union with `str` + | + 6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]] + 7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int] + 8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]] + | ^^^^^ PYI051 + 9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] +10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] + | + +PYI051.pyi:9:81: PYI051 `Literal["foo"]` is redundant in a union with `str` + | + 7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int] + 8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]] + 9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] + | ^^^^^ PYI051 +10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] + | + +PYI051.pyi:10:69: PYI051 `Literal["foo"]` is redundant in a union with `str` + | + 8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]] + 9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] +10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] + | ^^^^^ PYI051 +11 | +12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ... + | + PYI051.pyi:12:31: PYI051 `Literal[1J]` is redundant in a union with `complex` | 10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.py.snap index 64b9d9cfe2cace..1506146651bdfc 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.py.snap @@ -106,12 +106,12 @@ PYI055.py:50:9: PYI055 [*] Multiple `type` members in a union. Combine them into 52 52 | ... 53 53 | -PYI055.py:56:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`. +PYI055.py:56:9: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`. | 54 | def convert_union(union: UnionType) -> _T | None: 55 | converters: tuple[ 56 | Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 57 | ] = union.__args__ 58 | ... | @@ -122,17 +122,17 @@ PYI055.py:56:15: PYI055 [*] Multiple `type` members in a union. Combine them int 54 54 | def convert_union(union: UnionType) -> _T | None: 55 55 | converters: tuple[ 56 |- Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055 - 56 |+ Union[type[_T | Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055 + 56 |+ type[_T | Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055 57 57 | ] = union.__args__ 58 58 | ... 59 59 | -PYI055.py:62:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`. +PYI055.py:62:9: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`. | 60 | def convert_union(union: UnionType) -> _T | None: 61 | converters: tuple[ 62 | Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 63 | ] = union.__args__ 64 | ... | @@ -143,17 +143,17 @@ PYI055.py:62:15: PYI055 [*] Multiple `type` members in a union. Combine them int 60 60 | def convert_union(union: UnionType) -> _T | None: 61 61 | converters: tuple[ 62 |- Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055 - 62 |+ Union[type[_T | Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055 + 62 |+ type[_T | Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055 63 63 | ] = union.__args__ 64 64 | ... 65 65 | -PYI055.py:68:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`. +PYI055.py:68:9: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`. | 66 | def convert_union(union: UnionType) -> _T | None: 67 | converters: tuple[ 68 | Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 69 | ] = union.__args__ 70 | ... | @@ -164,6 +164,6 @@ PYI055.py:68:15: PYI055 [*] Multiple `type` members in a union. Combine them int 66 66 | def convert_union(union: UnionType) -> _T | None: 67 67 | converters: tuple[ 68 |- Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055 - 68 |+ Union[type[_T | Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055 + 68 |+ type[_T | Converter[_T]] | str | Converter[_T] | Callable[[str], _T], ... # PYI055 69 69 | ] = union.__args__ 70 70 | ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.pyi.snap index 6d8288d4fcaf33..2d647bbcce5141 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.pyi.snap @@ -105,12 +105,12 @@ PYI055.pyi:8:4: PYI055 [*] Multiple `type` members in a union. Combine them into 10 10 | y: Union[Union[Union[type[float, int], type[complex]]]] 11 11 | z: Union[type[complex], Union[Union[type[float, int]]]] -PYI055.pyi:9:10: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`. +PYI055.pyi:9:4: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`. | 7 | v: Union[type[float], type[complex]] 8 | w: Union[type[float, int], type[complex]] 9 | x: Union[Union[type[float, int], type[complex]]] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 10 | y: Union[Union[Union[type[float, int], type[complex]]]] 11 | z: Union[type[complex], Union[Union[type[float, int]]]] | @@ -121,11 +121,52 @@ PYI055.pyi:9:10: PYI055 [*] Multiple `type` members in a union. Combine them int 7 7 | v: Union[type[float], type[complex]] 8 8 | w: Union[type[float, int], type[complex]] 9 |-x: Union[Union[type[float, int], type[complex]]] - 9 |+x: Union[type[Union[float, int, complex]]] + 9 |+x: type[Union[float, int, complex]] 10 10 | y: Union[Union[Union[type[float, int], type[complex]]]] 11 11 | z: Union[type[complex], Union[Union[type[float, int]]]] 12 12 | +PYI055.pyi:10:4: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`. + | + 8 | w: Union[type[float, int], type[complex]] + 9 | x: Union[Union[type[float, int], type[complex]]] +10 | y: Union[Union[Union[type[float, int], type[complex]]]] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 +11 | z: Union[type[complex], Union[Union[type[float, int]]]] + | + = help: Combine multiple `type` members + +ℹ Safe fix +7 7 | v: Union[type[float], type[complex]] +8 8 | w: Union[type[float, int], type[complex]] +9 9 | x: Union[Union[type[float, int], type[complex]]] +10 |-y: Union[Union[Union[type[float, int], type[complex]]]] + 10 |+y: type[Union[float, int, complex]] +11 11 | z: Union[type[complex], Union[Union[type[float, int]]]] +12 12 | +13 13 | def func(arg: type[int] | str | type[float]) -> None: ... + +PYI055.pyi:11:4: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[Union[complex, float, int]]`. + | + 9 | x: Union[Union[type[float, int], type[complex]]] +10 | y: Union[Union[Union[type[float, int], type[complex]]]] +11 | z: Union[type[complex], Union[Union[type[float, int]]]] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055 +12 | +13 | def func(arg: type[int] | str | type[float]) -> None: ... + | + = help: Combine multiple `type` members + +ℹ Safe fix +8 8 | w: Union[type[float, int], type[complex]] +9 9 | x: Union[Union[type[float, int], type[complex]]] +10 10 | y: Union[Union[Union[type[float, int], type[complex]]]] +11 |-z: Union[type[complex], Union[Union[type[float, int]]]] + 11 |+z: type[Union[complex, float, int]] +12 12 | +13 13 | def func(arg: type[int] | str | type[float]) -> None: ... +14 14 | + PYI055.pyi:13:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[int | float]`. | 11 | z: Union[type[complex], Union[Union[type[float, int]]]] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap index 17be93901e8d61..a2f290e969b072 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap @@ -318,24 +318,6 @@ PYI062.py:25:46: PYI062 [*] Duplicate literal member `True` 27 27 | n: Literal["No", "duplicates", "here", 1, "1"] 28 28 | -PYI062.py:32:37: PYI062 [*] Duplicate literal member `1` - | -30 | # nested literals, all equivalent to `Literal[1]` -31 | Literal[Literal[1]] # no duplicate -32 | Literal[Literal[Literal[1], Literal[1]]] # once - | ^ PYI062 -33 | Literal[Literal[1], Literal[Literal[Literal[1]]]] # once - | - = help: Remove duplicates - -ℹ Safe fix -29 29 | -30 30 | # nested literals, all equivalent to `Literal[1]` -31 31 | Literal[Literal[1]] # no duplicate -32 |-Literal[Literal[Literal[1], Literal[1]]] # once - 32 |+Literal[Literal[1]] # once -33 33 | Literal[Literal[1], Literal[Literal[Literal[1]]]] # once - PYI062.py:32:37: PYI062 [*] Duplicate literal member `1` | 30 | # nested literals, all equivalent to `Literal[1]` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap index 7f893a8e5bf0fe..f03c08bc343ce3 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap @@ -318,24 +318,6 @@ PYI062.pyi:25:46: PYI062 [*] Duplicate literal member `True` 27 27 | n: Literal["No", "duplicates", "here", 1, "1"] 28 28 | -PYI062.pyi:32:37: PYI062 [*] Duplicate literal member `1` - | -30 | # nested literals, all equivalent to `Literal[1]` -31 | Literal[Literal[1]] # no duplicate -32 | Literal[Literal[Literal[1], Literal[1]]] # once - | ^ PYI062 -33 | Literal[Literal[1], Literal[Literal[Literal[1]]]] # once - | - = help: Remove duplicates - -ℹ Safe fix -29 29 | -30 30 | # nested literals, all equivalent to `Literal[1]` -31 31 | Literal[Literal[1]] # no duplicate -32 |-Literal[Literal[Literal[1], Literal[1]]] # once - 32 |+Literal[Literal[1]] # once -33 33 | Literal[Literal[1], Literal[Literal[Literal[1]]]] # once - PYI062.pyi:32:37: PYI062 [*] Duplicate literal member `1` | 30 | # nested literals, all equivalent to `Literal[1]` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF041_RUF041.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF041_RUF041.py.snap index 9de9644bcfa137..922ac7747dfdb5 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF041_RUF041.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF041_RUF041.py.snap @@ -216,26 +216,6 @@ RUF041.py:24:1: RUF041 [*] Unnecessary nested `Literal` 26 26 | 27 27 | # OK -RUF041.py:24:9: RUF041 [*] Unnecessary nested `Literal` - | -22 | # nested literals, all equivalent to `Literal[1]` -23 | Literal[Literal[1]] -24 | Literal[Literal[Literal[1], Literal[1]]] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF041 -25 | Literal[Literal[1], Literal[Literal[Literal[1]]]] - | - = help: Replace with flattened `Literal` - -ℹ Safe fix -21 21 | -22 22 | # nested literals, all equivalent to `Literal[1]` -23 23 | Literal[Literal[1]] -24 |-Literal[Literal[Literal[1], Literal[1]]] - 24 |+Literal[Literal[1, 1]] -25 25 | Literal[Literal[1], Literal[Literal[Literal[1]]]] -26 26 | -27 27 | # OK - RUF041.py:25:1: RUF041 [*] Unnecessary nested `Literal` | 23 | Literal[Literal[1]] @@ -256,24 +236,3 @@ RUF041.py:25:1: RUF041 [*] Unnecessary nested `Literal` 26 26 | 27 27 | # OK 28 28 | x: Literal[True, False, True, False] - -RUF041.py:25:29: RUF041 [*] Unnecessary nested `Literal` - | -23 | Literal[Literal[1]] -24 | Literal[Literal[Literal[1], Literal[1]]] -25 | Literal[Literal[1], Literal[Literal[Literal[1]]]] - | ^^^^^^^^^^^^^^^^^^^ RUF041 -26 | -27 | # OK - | - = help: Replace with flattened `Literal` - -ℹ Safe fix -22 22 | # nested literals, all equivalent to `Literal[1]` -23 23 | Literal[Literal[1]] -24 24 | Literal[Literal[Literal[1], Literal[1]]] -25 |-Literal[Literal[1], Literal[Literal[Literal[1]]]] - 25 |+Literal[Literal[1], Literal[Literal[1]]] -26 26 | -27 27 | # OK -28 28 | x: Literal[True, False, True, False] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF041_RUF041.pyi.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF041_RUF041.pyi.snap index c2f26280bf1596..14c703e88813e9 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF041_RUF041.pyi.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF041_RUF041.pyi.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs -snapshot_kind: text --- RUF041.pyi:6:4: RUF041 [*] Unnecessary nested `Literal` | @@ -216,26 +215,6 @@ RUF041.pyi:24:1: RUF041 [*] Unnecessary nested `Literal` 26 26 | 27 27 | # OK -RUF041.pyi:24:9: RUF041 [*] Unnecessary nested `Literal` - | -22 | # nested literals, all equivalent to `Literal[1]` -23 | Literal[Literal[1]] -24 | Literal[Literal[Literal[1], Literal[1]]] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF041 -25 | Literal[Literal[1], Literal[Literal[Literal[1]]]] - | - = help: Replace with flattened `Literal` - -ℹ Safe fix -21 21 | -22 22 | # nested literals, all equivalent to `Literal[1]` -23 23 | Literal[Literal[1]] -24 |-Literal[Literal[Literal[1], Literal[1]]] - 24 |+Literal[Literal[1, 1]] -25 25 | Literal[Literal[1], Literal[Literal[Literal[1]]]] -26 26 | -27 27 | # OK - RUF041.pyi:25:1: RUF041 [*] Unnecessary nested `Literal` | 23 | Literal[Literal[1]] @@ -256,24 +235,3 @@ RUF041.pyi:25:1: RUF041 [*] Unnecessary nested `Literal` 26 26 | 27 27 | # OK 28 28 | x: Literal[True, False, True, False] - -RUF041.pyi:25:29: RUF041 [*] Unnecessary nested `Literal` - | -23 | Literal[Literal[1]] -24 | Literal[Literal[Literal[1], Literal[1]]] -25 | Literal[Literal[1], Literal[Literal[Literal[1]]]] - | ^^^^^^^^^^^^^^^^^^^ RUF041 -26 | -27 | # OK - | - = help: Replace with flattened `Literal` - -ℹ Safe fix -22 22 | # nested literals, all equivalent to `Literal[1]` -23 23 | Literal[Literal[1]] -24 24 | Literal[Literal[Literal[1], Literal[1]]] -25 |-Literal[Literal[1], Literal[Literal[Literal[1]]]] - 25 |+Literal[Literal[1], Literal[Literal[1]]] -26 26 | -27 27 | # OK -28 28 | x: Literal[True, False, True, False] diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 37f74fa8f51a6e..9cf8888b72f7db 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -373,7 +373,7 @@ where ) where F: FnMut(&'a Expr, &'a Expr), { - // Ex) x | y + // Ex) `x | y` if let Expr::BinOp(ast::ExprBinOp { op: Operator::BitOr, left, @@ -396,17 +396,21 @@ where return; } - // Ex) Union[x, y] + // Ex) `Union[x, y]` if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { if semantic.match_typing_expr(value, "Union") { if let Expr::Tuple(tuple) = &**slice { // Traverse each element of the tuple within the union recursively to handle cases - // such as `Union[..., Union[...]] + // such as `Union[..., Union[...]]` tuple .iter() .for_each(|elem| inner(func, semantic, elem, Some(expr))); return; } + + // Ex) `Union[Union[a, b]]` and `Union[a | b | c]` + inner(func, semantic, slice, Some(expr)); + return; } } @@ -435,18 +439,19 @@ where ) where F: FnMut(&'a Expr, &'a Expr), { - // Ex) Literal[x, y] + // Ex) `Literal[x, y]` if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { if semantic.match_typing_expr(value, "Literal") { match &**slice { Expr::Tuple(tuple) => { // Traverse each element of the tuple within the literal recursively to handle cases - // such as `Literal[..., Literal[...]] + // such as `Literal[..., Literal[...]]` for element in tuple { inner(func, semantic, element, Some(expr)); } } other => { + // Ex) `Literal[Literal[...]]` inner(func, semantic, other, Some(expr)); } } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index d93e12e6f60deb..21d8c41f87140f 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -5,7 +5,7 @@ use rustc_hash::FxHashMap; use ruff_python_ast::helpers::from_relative_import; use ruff_python_ast::name::{QualifiedName, UnqualifiedName}; -use ruff_python_ast::{self as ast, Expr, ExprContext, Operator, PySourceType, Stmt}; +use ruff_python_ast::{self as ast, Expr, ExprContext, PySourceType, Stmt}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::binding::{ @@ -1506,38 +1506,48 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the model is in a nested union expression (e.g., the inner `Union` in /// `Union[Union[int, str], float]`). pub fn in_nested_union(&self) -> bool { - // Ex) `Union[Union[int, str], float]` - if self - .current_expression_grandparent() - .and_then(Expr::as_subscript_expr) - .is_some_and(|parent| self.match_typing_expr(&parent.value, "Union")) - { - return true; - } - - // Ex) `int | Union[str, float]` - if self.current_expression_parent().is_some_and(|parent| { - matches!( - parent, - Expr::BinOp(ast::ExprBinOp { - op: Operator::BitOr, - .. - }) - ) - }) { - return true; + let mut parent_expressions = self.current_expressions().skip(1); + + match parent_expressions.next() { + // The parent expression is of the inner union is a single `typing.Union`. + // Ex) `Union[Union[a, b]]` + Some(Expr::Subscript(parent)) => self.match_typing_expr(&parent.value, "Union"), + // The parent expression is of the inner union is a tuple with two or more + // comma-separated elements and the parent of that tuple is a `typing.Union`. + // Ex) `Union[Union[a, b], Union[c, d]]` + Some(Expr::Tuple(_)) => parent_expressions + .next() + .and_then(Expr::as_subscript_expr) + .is_some_and(|grandparent| self.match_typing_expr(&grandparent.value, "Union")), + // The parent expression of the inner union is a PEP604-style union. + // Ex) `a | b | c` or `Union[a, b] | c` + // In contrast to `typing.Union`, PEP604-style unions are always binary operations, e.g. + // the expression `a | b | c` is represented by two binary unions: `(a | b) | c`. + Some(Expr::BinOp(bin_op)) => bin_op.op.is_bit_or(), + // Not a nested union otherwise. + _ => false, } - - false } /// Return `true` if the model is in a nested literal expression (e.g., the inner `Literal` in /// `Literal[Literal[int, str], float]`). pub fn in_nested_literal(&self) -> bool { - // Ex) `Literal[Literal[int, str], float]` - self.current_expression_grandparent() - .and_then(Expr::as_subscript_expr) - .is_some_and(|parent| self.match_typing_expr(&parent.value, "Literal")) + let mut parent_expressions = self.current_expressions().skip(1); + + match parent_expressions.next() { + // The parent expression of the current `Literal` is a tuple, and the + // grandparent is a `Literal`. + // Ex) `Literal[Literal[str], Literal[int]]` + Some(Expr::Tuple(_)) => parent_expressions + .next() + .and_then(Expr::as_subscript_expr) + .is_some_and(|grandparent| self.match_typing_expr(&grandparent.value, "Literal")), + // The parent expression of the current `Literal` is also a `Literal`. + // Ex) `Literal[Literal[str]]` + Some(Expr::Subscript(parent)) => self.match_typing_expr(&parent.value, "Literal"), + // Not a nested literal otherwise + _ => false, + } } /// Returns `true` if `left` and `right` are in the same branches of an `if`, `match`, or