From ac75e377b8c6213f549fccc45a4d34db2c73404c Mon Sep 17 00:00:00 2001 From: Sam Wellander Date: Thu, 21 Oct 2021 21:56:55 -0700 Subject: [PATCH] feat!: add prefer `TypeError` analyzer --- src/tryceratops/analyzers/__init__.py | 3 +- src/tryceratops/analyzers/conditional.py | 64 ++++++++++++++++++++++++ src/tryceratops/violations/codes.py | 1 + 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/tryceratops/analyzers/conditional.py diff --git a/src/tryceratops/analyzers/__init__.py b/src/tryceratops/analyzers/__init__.py index 0cc5929..db8ac17 100644 --- a/src/tryceratops/analyzers/__init__.py +++ b/src/tryceratops/analyzers/__init__.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Set -from . import call, exception_block, try_block +from . import call, conditional, exception_block, try_block from .base import BaseAnalyzer if TYPE_CHECKING: @@ -14,6 +14,7 @@ call.CallRaiseVanillaAnalyzer, # type: ignore call.CallRaiseLongArgsAnalyzer, # type: ignore call.CallAvoidCheckingToContinueAnalyzer, # type: ignore + conditional.PreferTypeErrorAnalyzer, exception_block.ExceptReraiseWithoutCauseAnalyzer, exception_block.ExceptVerboseReraiseAnalyzer, exception_block.ExceptBroadPassAnalyzer, diff --git a/src/tryceratops/analyzers/conditional.py b/src/tryceratops/analyzers/conditional.py new file mode 100644 index 0000000..710ac7f --- /dev/null +++ b/src/tryceratops/analyzers/conditional.py @@ -0,0 +1,64 @@ +import ast +from typing import Union + +from tryceratops.violations import codes + +from .base import BaseAnalyzer + +STANDARD_NON_TYPE_ERROR_IDS = ( + "ArithmeticError", + "AssertionError", + "AttributeError", + "BufferError", + "EOFError", + "Exception", + "ImportError", + "LookupError", + "MemoryError", + "NameError", + "ReferenceError", + "RuntimeError", + "SyntaxError", + "SystemError", + "ValueError", +) + + +class PreferTypeErrorAnalyzer(BaseAnalyzer): + violation_code = codes.PREFER_TYPE_ERROR + + def _is_checking_type(self, node: Union[ast.stmt, ast.expr]) -> bool: + if isinstance(node, ast.If): + return self._is_checking_type(node.test) and all( + [self._is_checking_type(stm) for stm in node.orelse if isinstance(stm, ast.If)] + ) + if isinstance(node, ast.UnaryOp): + return self._is_checking_type(node.operand) + if isinstance(node, ast.BoolOp): + return all([self._is_checking_type(value) for value in node.values]) + if isinstance(node, ast.Call): + if isinstance(node.func, ast.Name): + if node.func.id in ("isinstance", "issubclass", "callable"): + return True + return False + + def _check_is_raise_other_than_typeerror(self, node: ast.Raise) -> None: + if isinstance(node.exc, ast.Call): + if isinstance(node.exc.func, ast.Name): + if node.exc.func.id in STANDARD_NON_TYPE_ERROR_IDS: + self._mark_violation(node.exc.func) + elif isinstance(node.exc, ast.Name): + if node.exc.id in STANDARD_NON_TYPE_ERROR_IDS: + self._mark_violation(node.exc) + + def _check_for_raises(self, node): + for stm in ast.iter_child_nodes(node): + if isinstance(stm, ast.Raise): + self._check_is_raise_other_than_typeerror(stm) + if isinstance(node, ast.If): + for stm in node.orelse: + self._check_for_raises(stm) + + def visit_If(self, node: ast.If) -> None: + if self._is_checking_type(node): + self._check_for_raises(node) diff --git a/src/tryceratops/violations/codes.py b/src/tryceratops/violations/codes.py index 4b02d30..4a625fe 100644 --- a/src/tryceratops/violations/codes.py +++ b/src/tryceratops/violations/codes.py @@ -4,6 +4,7 @@ "TC003", "Avoid specifying long messages outside the exception class", ) +PREFER_TYPE_ERROR = ("TC004", "Prefer TypeError exception for invalid type") # TC1xx - General CHECK_TO_CONTINUE = (