From 29150d28557cb18e92cdec362534d47c42328474 Mon Sep 17 00:00:00 2001 From: yangdanny97 Date: Thu, 2 Jan 2025 21:37:15 -0500 Subject: [PATCH 1/8] variadic callable --- docs/guide/builtins.rst | 20 ++- src/fixit/rules/variadic_callable_syntax.py | 134 ++++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/fixit/rules/variadic_callable_syntax.py diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index 85cce4d6..c59049d4 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -43,6 +43,7 @@ Built-in Rules - :class:`UseClsInClassmethod` - :class:`UseFstring` - :class:`UseTypesFromTyping` +- :class:`VariadicCallableSyntax` .. class:: AvoidOrInExcept @@ -1492,4 +1493,21 @@ Built-in Rules from fixit import LintRule class CatsRuleDogsDroolRule(LintRule): ... - \ No newline at end of file + +.. class:: VariadicCallableSyntax + + Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]` + + .. attribute:: AUTOFIX + :type: Yes + + .. attribute:: VALID + + .. code:: python + from typing import Callable + x: Callable[..., int] + .. attribute:: INVALID + + .. code:: python + from typing import Callable + x: Callable[[...], int] diff --git a/src/fixit/rules/variadic_callable_syntax.py b/src/fixit/rules/variadic_callable_syntax.py new file mode 100644 index 00000000..e167c04f --- /dev/null +++ b/src/fixit/rules/variadic_callable_syntax.py @@ -0,0 +1,134 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +import libcst as cst +import libcst.matchers as m +from libcst.metadata import QualifiedName, QualifiedNameProvider, QualifiedNameSource + +from fixit import Invalid, LintRule, Valid + + +class VariadicCallableSyntax(LintRule): + """ + Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]` + """ + + METADATA_DEPENDENCIES = (QualifiedNameProvider,) + VALID = [ + Valid( + """ + from typing import Callable + x: Callable[[int], int] + """ + ), + Valid( + """ + from typing import Callable + x: Callable[[int, int, ...], int] + """ + ), + Valid( + """ + from typing import Callable + x: Callable + """ + ), + Valid( + """ + from typing import Callable as C + x: C[..., int] = ... + """ + ), + Valid( + """ + from typing import Callable + def foo(bar: Optional[Callable[..., int]]) -> Callable[..., int]: + ... + """ + ), + Valid( + """ + import typing as t + x: t.Callable[..., int] = ... + """ + ), + Valid( + """ + from typing import Callable + x: Callable[..., int] = ... + """ + ), + ] + INVALID = [ + Invalid( + """ + from typing import Callable + x: Callable[[...], int] = ... + """, + expected_replacement=""" + from typing import Callable + x: Callable[..., int] = ... + """, + ), + Invalid( + """ + import typing as t + x: t.Callable[[...], int] = ... + """, + expected_replacement=""" + import typing as t + x: t.Callable[..., int] = ... + """, + ), + Invalid( + """ + from typing import Callable as C + x: C[[...], int] = ... + """, + expected_replacement=""" + from typing import Callable as C + x: C[..., int] = ... + """, + ), + Invalid( + """ + from typing import Callable + def foo(bar: Optional[Callable[[...], int]]) -> Callable[[...], int]: + ... + """, + expected_replacement=""" + from typing import Callable + def foo(bar: Optional[Callable[..., int]]) -> Callable[..., int]: + ... + """, + ), + ] + + def __init__(self) -> None: + super().__init__() + + def visit_Subscript(self, node: cst.Subscript) -> None: + if not QualifiedNameProvider.has_name( + self, + node, + QualifiedName(name="typing.Callable", source=QualifiedNameSource.IMPORT), + ): + return + node_matches = len(node.slice) == 2 and m.matches( + node.slice[0], + m.SubscriptElement( + slice=m.Index(value=m.List(elements=[m.Element(m.Ellipsis())])) + ), + ) + if not node_matches: + return + slices = list(node.slice) + slices[0] = cst.SubscriptElement(cst.Index(cst.Ellipsis())) + new_node = node.with_changes(slice=slices) + self.report( + node, + "Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]`", + replacement=node.deep_replace(node, new_node), + ) From 592c2c1fc95cf662f099c56e7beaac642eec1760 Mon Sep 17 00:00:00 2001 From: yangdanny97 Date: Thu, 2 Jan 2025 22:07:19 -0500 Subject: [PATCH 2/8] move docs --- docs/guide/builtins.rst | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index c59049d4..77ee6428 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -1252,6 +1252,24 @@ Built-in Rules def function(list: list[str]) -> None: pass +.. class:: VariadicCallableSyntax + + Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]` + + .. attribute:: AUTOFIX + :type: Yes + + .. attribute:: VALID + + .. code:: python + from typing import Callable + x: Callable[..., int] + .. attribute:: INVALID + + .. code:: python + from typing import Callable + x: Callable[[...], int] + ``fixit.rules.extra`` ^^^^^^^^^^^^^^^^^^^^^ @@ -1493,21 +1511,3 @@ Built-in Rules from fixit import LintRule class CatsRuleDogsDroolRule(LintRule): ... - -.. class:: VariadicCallableSyntax - - Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]` - - .. attribute:: AUTOFIX - :type: Yes - - .. attribute:: VALID - - .. code:: python - from typing import Callable - x: Callable[..., int] - .. attribute:: INVALID - - .. code:: python - from typing import Callable - x: Callable[[...], int] From fcb3f0c157622899ee85ba86796f6967ff0a9cbb Mon Sep 17 00:00:00 2001 From: yangdanny97 Date: Thu, 2 Jan 2025 22:07:38 -0500 Subject: [PATCH 3/8] spacing --- docs/guide/builtins.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index 77ee6428..7c968831 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -1264,6 +1264,7 @@ Built-in Rules .. code:: python from typing import Callable x: Callable[..., int] + .. attribute:: INVALID .. code:: python From d98c18feed51981c5cc5710879fb516b6cad93bc Mon Sep 17 00:00:00 2001 From: yangdanny97 Date: Thu, 2 Jan 2025 22:55:24 -0500 Subject: [PATCH 4/8] fix docs --- docs/guide/builtins.rst | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index 7c968831..951e669f 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -1251,7 +1251,6 @@ Built-in Rules def function(list: list[str]) -> None: pass - .. class:: VariadicCallableSyntax Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]` @@ -1259,17 +1258,38 @@ Built-in Rules .. attribute:: AUTOFIX :type: Yes + .. attribute:: VALID .. code:: python + + from typing import Callable + x: Callable[[int], int] + .. code:: python + from typing import Callable - x: Callable[..., int] + x: Callable[[int, int, ...], int] .. attribute:: INVALID .. code:: python + + from typing import Callable + x: Callable[[...], int] = ... + + # suggested fix from typing import Callable - x: Callable[[...], int] + x: Callable[..., int] = ... + + .. code:: python + + import typing as t + x: t.Callable[[...], int] = ... + + # suggested fix + import typing as t + x: t.Callable[..., int] = ... + ``fixit.rules.extra`` ^^^^^^^^^^^^^^^^^^^^^ From 7f093de918f0357cf189cbb6bfc97c9f550b5ea6 Mon Sep 17 00:00:00 2001 From: yangdanny97 Date: Fri, 3 Jan 2025 12:27:19 -0500 Subject: [PATCH 5/8] spacing --- docs/guide/builtins.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index 951e669f..d3c64f47 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -1532,3 +1532,4 @@ Built-in Rules from fixit import LintRule class CatsRuleDogsDroolRule(LintRule): ... + From 22234dbc01dae1b9c0d5f8d1acdb1351fe990554 Mon Sep 17 00:00:00 2001 From: yangdanny97 Date: Fri, 3 Jan 2025 14:06:40 -0500 Subject: [PATCH 6/8] spacing --- docs/guide/builtins.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index d3c64f47..b66f3994 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -1532,4 +1532,4 @@ Built-in Rules from fixit import LintRule class CatsRuleDogsDroolRule(LintRule): ... - + \ No newline at end of file From 55e253eddd252cb6414aa55507ec825bed19a1ae Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 4 Jun 2025 16:19:42 -0700 Subject: [PATCH 7/8] Cleanup code, reword lint message --- src/fixit/rules/variadic_callable_syntax.py | 27 +++++++++------------ 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/fixit/rules/variadic_callable_syntax.py b/src/fixit/rules/variadic_callable_syntax.py index e167c04f..07febb8b 100644 --- a/src/fixit/rules/variadic_callable_syntax.py +++ b/src/fixit/rules/variadic_callable_syntax.py @@ -12,7 +12,7 @@ class VariadicCallableSyntax(LintRule): """ - Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]` + Callable types with arbitrary parameters should be written as `Callable[..., T]` """ METADATA_DEPENDENCIES = (QualifiedNameProvider,) @@ -106,9 +106,6 @@ def foo(bar: Optional[Callable[..., int]]) -> Callable[..., int]: ), ] - def __init__(self) -> None: - super().__init__() - def visit_Subscript(self, node: cst.Subscript) -> None: if not QualifiedNameProvider.has_name( self, @@ -116,19 +113,17 @@ def visit_Subscript(self, node: cst.Subscript) -> None: QualifiedName(name="typing.Callable", source=QualifiedNameSource.IMPORT), ): return - node_matches = len(node.slice) == 2 and m.matches( + if len(node.slice) == 2 and m.matches( node.slice[0], m.SubscriptElement( slice=m.Index(value=m.List(elements=[m.Element(m.Ellipsis())])) ), - ) - if not node_matches: - return - slices = list(node.slice) - slices[0] = cst.SubscriptElement(cst.Index(cst.Ellipsis())) - new_node = node.with_changes(slice=slices) - self.report( - node, - "Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]`", - replacement=node.deep_replace(node, new_node), - ) + ): + slices = list(node.slice) + slices[0] = cst.SubscriptElement(cst.Index(cst.Ellipsis())) + new_node = node.with_changes(slice=slices) + self.report( + node, + self.__doc__, + replacement=node.deep_replace(node, new_node), + ) From ca54763264939faa603a51df49ae66ca4e8f8dac Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 4 Jun 2025 16:29:56 -0700 Subject: [PATCH 8/8] Regen docs --- docs/guide/builtins.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index b66f3994..cd9671d1 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -1253,13 +1253,15 @@ Built-in Rules pass .. class:: VariadicCallableSyntax - Callable types with arbitrary parameters are written as `Callable[..., T]`, not `Callable[[...], T]` + Callable types with arbitrary parameters should be written as `Callable[..., T]` .. attribute:: AUTOFIX + :no-index: :type: Yes .. attribute:: VALID + :no-index: .. code:: python @@ -1271,6 +1273,7 @@ Built-in Rules x: Callable[[int, int, ...], int] .. attribute:: INVALID + :no-index: .. code:: python