From de5fc1472c3671a27fdc02c8df42612452d6f4c8 Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Mon, 18 Nov 2024 17:05:15 -0500 Subject: [PATCH 1/4] add builtin types linter --- docs/guide/builtins.rst | 47 ++++++++++ src/fixit/rules/use_builtin_types.py | 124 +++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 src/fixit/rules/use_builtin_types.py diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index 6f2b1484..873ee92a 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -40,6 +40,7 @@ Built-in Rules - :class:`UseAssertIn` - :class:`UseAssertIsNotNone` - :class:`UseAsyncSleepInAsyncDef` +- :class:`UseBuiltinTypes` - :class:`UseClsInClassmethod` - :class:`UseFstring` - :class:`UseTypesFromTyping` @@ -1007,6 +1008,52 @@ Built-in Rules from time import sleep async def func(): sleep(1) +.. class:: UseBuiltinTypes + + Enforces the use of builtin types instead of their aliases from the ``typing`` + module in Python 3.10 and later. + + .. attribute:: AUTOFIX + :type: Yes + + .. attribute:: PYTHON_VERSION + :type: '>= 3.10' + + .. attribute:: VALID + + .. code:: python + + def fuction(list: list[str]) -> None: + pass + .. code:: python + + def function() -> None: + thing: dict[str, str] = {} + + .. attribute:: INVALID + + .. code:: python + + from typing import List + def whatever(list: List[str]) -> None: + pass + + # suggested fix + from typing import List + def whatever(list: list[str]) -> None: + pass + + .. code:: python + + from typing import Dict + def func() -> None: + thing: Dict[str, str] = {} + + # suggested fix + from typing import Dict + def func() -> None: + thing: dict[str, str] = {} + .. class:: UseClsInClassmethod Enforces using ``cls`` as the first argument in a ``@classmethod``. diff --git a/src/fixit/rules/use_builtin_types.py b/src/fixit/rules/use_builtin_types.py new file mode 100644 index 00000000..fae270ff --- /dev/null +++ b/src/fixit/rules/use_builtin_types.py @@ -0,0 +1,124 @@ +# 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. + +from typing import Set + +import libcst + +from fixit import Invalid, LintRule, Valid +from libcst.metadata import QualifiedNameProvider, ScopeProvider + + +REPLACE_TYPING_TYPE_ANNOTATION: str = ( + "You are using typing.{typing_type} as a type annotation " + + "but you should use {correct_type} instead." +) + +TYPING_TYPE_TO_REPLACE: Set[str] = {"Dict", "List", "Set", "Tuple", "Type"} +QUALIFIED_TYPES_TO_REPLACE: Set[str] = {f"typing.{s}" for s in TYPING_TYPE_TO_REPLACE} + + +class UseBuiltinTypes(LintRule): + """ + Enforces the use of builtin types instead of their aliases from the ``typing`` + module in Python 3.10 and later. + """ + + PYTHON_VERSION = ">= 3.10" + + METADATA_DEPENDENCIES = ( + QualifiedNameProvider, + ScopeProvider, + ) + VALID = [ + Valid( + """ + def fuction(list: list[str]) -> None: + pass + """ + ), + Valid( + """ + def function() -> None: + thing: dict[str, str] = {} + """ + ), + Valid( + """ + def function() -> None: + thing: tuple[str] + """ + ), + ] + INVALID = [ + Invalid( + """ + from typing import List + def whatever(list: List[str]) -> None: + pass + """, + expected_replacement=""" + from typing import List + def whatever(list: list[str]) -> None: + pass + """, + ), + Invalid( + """ + from typing import Dict + def func() -> None: + thing: Dict[str, str] = {} + """, + expected_replacement=""" + from typing import Dict + def func() -> None: + thing: dict[str, str] = {} + """, + ), + Invalid( + """ + from typing import Tuple + def func() -> None: + thing: Tuple[str] + """, + expected_replacement=""" + from typing import Tuple + def func() -> None: + thing: tuple[str] + """, + ), + ] + + def __init__(self) -> None: + super().__init__() + self.annotation_counter: int = 0 + + def visit_Annotation(self, node: libcst.Annotation) -> None: + self.annotation_counter += 1 + + def leave_Annotation(self, original_node: libcst.Annotation) -> None: + self.annotation_counter -= 1 + + def visit_Name(self, node: libcst.Name) -> None: + qualified_names = self.get_metadata(QualifiedNameProvider, node, set()) + + is_typing_type = node.value in TYPING_TYPE_TO_REPLACE and all( + qualified_name.name in QUALIFIED_TYPES_TO_REPLACE + for qualified_name in qualified_names + ) + + if self.annotation_counter > 0 and is_typing_type: + correct_type = node.value.title() + scope = self.get_metadata(ScopeProvider, node) + replacement = None + if scope is not None and correct_type in scope: + replacement = node.with_changes(value=correct_type) + self.report( + node, + REPLACE_TYPING_TYPE_ANNOTATION.format( + typing_type=node.value, correct_type=correct_type + ), + replacement=replacement, + ) From eddf8d8a872484f71f252b4872d977b601277b32 Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Mon, 18 Nov 2024 17:11:37 -0500 Subject: [PATCH 2/4] fix correct type --- src/fixit/rules/use_builtin_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixit/rules/use_builtin_types.py b/src/fixit/rules/use_builtin_types.py index fae270ff..1beadf19 100644 --- a/src/fixit/rules/use_builtin_types.py +++ b/src/fixit/rules/use_builtin_types.py @@ -110,7 +110,7 @@ def visit_Name(self, node: libcst.Name) -> None: ) if self.annotation_counter > 0 and is_typing_type: - correct_type = node.value.title() + correct_type = node.value.title().lower() scope = self.get_metadata(ScopeProvider, node) replacement = None if scope is not None and correct_type in scope: From dd37f66e5489abd23aafbf6ffba0842780e10654 Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Tue, 19 Nov 2024 10:16:12 -0500 Subject: [PATCH 3/4] change fixit config to 3.8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5eb0080c..f3689299 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ target-version = ["py38"] [tool.fixit] enable = ["fixit.rules"] -python-version = "3.10" +python-version = "3.8" formatter = "ufmt" [[tool.fixit.overrides]] From 1afc37a27acb3bf2c85e3c74d302bf4670176fa6 Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Tue, 19 Nov 2024 10:21:12 -0500 Subject: [PATCH 4/4] format --- src/fixit/rules/use_builtin_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixit/rules/use_builtin_types.py b/src/fixit/rules/use_builtin_types.py index 1beadf19..223ea0e2 100644 --- a/src/fixit/rules/use_builtin_types.py +++ b/src/fixit/rules/use_builtin_types.py @@ -6,9 +6,9 @@ from typing import Set import libcst +from libcst.metadata import QualifiedNameProvider, ScopeProvider from fixit import Invalid, LintRule, Valid -from libcst.metadata import QualifiedNameProvider, ScopeProvider REPLACE_TYPING_TYPE_ANNOTATION: str = (