Skip to content

Commit

Permalink
add builtin types linter
Browse files Browse the repository at this point in the history
  • Loading branch information
yangdanny97 committed Nov 18, 2024
1 parent 8430224 commit de5fc14
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 0 deletions.
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
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 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,
)

0 comments on commit de5fc14

Please sign in to comment.