Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace typing.List and other aliases w/ builtin equivalents after Python 3.10 #496

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/guide/builtins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Built-in Rules
- :class:`UseAssertIn`
- :class:`UseAssertIsNotNone`
- :class:`UseAsyncSleepInAsyncDef`
- :class:`UseBuiltinTypes`
- :class:`UseClsInClassmethod`
- :class:`UseFstring`
- :class:`UseTypesFromTyping`
Expand Down Expand Up @@ -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``.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
124 changes: 124 additions & 0 deletions src/fixit/rules/use_builtin_types.py
Original file line number Diff line number Diff line change
@@ -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 libcst.metadata import QualifiedNameProvider, ScopeProvider

from fixit import Invalid, LintRule, Valid


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().lower()
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,
)
Loading