mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
161 lines
5.8 KiB
Python
161 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
from collections.abc import Generator
|
|
from typing import Any
|
|
|
|
S001_fmt = (
|
|
"S001 Avoid using the {} mock call as it is "
|
|
"confusing and prone to causing invalid test "
|
|
"behavior."
|
|
)
|
|
S001_methods = frozenset(("not_called", "called_once", "called_once_with"))
|
|
|
|
S002_msg = "S002 print functions or statements are not allowed."
|
|
|
|
S003_msg = "S003 Use ``from sentry.utils import json`` instead."
|
|
S003_modules = frozenset(("json", "simplejson"))
|
|
|
|
S004_msg = "S004 Use `pytest.raises` instead for better debuggability."
|
|
S004_methods = frozenset(("assertRaises", "assertRaisesRegex"))
|
|
|
|
S005_msg = "S005 Do not import models from sentry.models but the actual module"
|
|
|
|
S006_msg = "S006 Do not use force_bytes / force_str -- test the types directly"
|
|
|
|
S007_msg = "S007 Do not import sentry.testutils into production code."
|
|
|
|
S008_msg = "S008 Use stdlib datetime.timezone.utc instead of pytz.utc / pytz.UTC"
|
|
|
|
S009_msg = "S009 Use `raise` with no arguments to reraise exceptions"
|
|
|
|
S010_msg = "S010 Except handler does nothing and should be removed"
|
|
|
|
S011_msg = "S011 Use override_options(...) instead to ensure proper cleanup"
|
|
|
|
|
|
class SentryVisitor(ast.NodeVisitor):
|
|
def __init__(self, filename: str) -> None:
|
|
self.errors: list[tuple[int, int, str]] = []
|
|
self.filename = filename
|
|
|
|
self._except_vars: list[str | None] = []
|
|
|
|
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
if node.module and not node.level:
|
|
if node.module.split(".")[0] in S003_modules:
|
|
self.errors.append((node.lineno, node.col_offset, S003_msg))
|
|
elif node.module == "sentry.models":
|
|
self.errors.append((node.lineno, node.col_offset, S005_msg))
|
|
elif (
|
|
"tests/" in self.filename
|
|
and node.module == "django.utils.encoding"
|
|
and any(x.name in {"force_bytes", "force_str"} for x in node.names)
|
|
):
|
|
self.errors.append((node.lineno, node.col_offset, S006_msg))
|
|
elif (
|
|
"tests/" not in self.filename
|
|
and "fixtures/" not in self.filename
|
|
and "sentry/testutils/" not in self.filename
|
|
and "sentry.testutils" in node.module
|
|
):
|
|
self.errors.append((node.lineno, node.col_offset, S007_msg))
|
|
|
|
if node.module == "pytz" and any(x.name.lower() == "utc" for x in node.names):
|
|
self.errors.append((node.lineno, node.col_offset, S008_msg))
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_Import(self, node: ast.Import) -> None:
|
|
for alias in node.names:
|
|
if alias.name.split(".")[0] in S003_modules:
|
|
self.errors.append((node.lineno, node.col_offset, S003_msg))
|
|
elif (
|
|
"tests/" not in self.filename
|
|
and "fixtures/" not in self.filename
|
|
and "sentry/testutils/" not in self.filename
|
|
and "sentry.testutils" in alias.name
|
|
):
|
|
self.errors.append((node.lineno, node.col_offset, S007_msg))
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_Attribute(self, node: ast.Attribute) -> None:
|
|
if node.attr in S001_methods:
|
|
self.errors.append((node.lineno, node.col_offset, S001_fmt.format(node.attr)))
|
|
elif node.attr in S004_methods:
|
|
self.errors.append((node.lineno, node.col_offset, S004_msg))
|
|
elif (
|
|
isinstance(node.value, ast.Name)
|
|
and node.value.id == "pytz"
|
|
and node.attr.lower() == "utc"
|
|
):
|
|
self.errors.append((node.lineno, node.col_offset, S008_msg))
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_Name(self, node: ast.Name) -> None:
|
|
if node.id == "print":
|
|
self.errors.append((node.lineno, node.col_offset, S002_msg))
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
self._except_vars.append(node.name)
|
|
try:
|
|
self.generic_visit(node)
|
|
finally:
|
|
self._except_vars.pop()
|
|
|
|
def visit_Raise(self, node: ast.Raise) -> None:
|
|
if (
|
|
self._except_vars
|
|
and isinstance(node.exc, ast.Name)
|
|
and node.exc.id == self._except_vars[-1]
|
|
):
|
|
self.errors.append((node.lineno, node.col_offset, S009_msg))
|
|
self.generic_visit(node)
|
|
|
|
def visit_Try(self, node: ast.Try) -> None:
|
|
if (
|
|
node.handlers
|
|
and len(node.handlers[-1].body) == 1
|
|
and isinstance(node.handlers[-1].body[0], ast.Raise)
|
|
and node.handlers[-1].body[0].exc is None
|
|
):
|
|
self.errors.append((node.handlers[-1].lineno, node.handlers[-1].col_offset, S010_msg))
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_Call(self, node: ast.Call) -> None:
|
|
if (
|
|
# override_settings(...)
|
|
(isinstance(node.func, ast.Name) and node.func.id == "override_settings")
|
|
or
|
|
# self.settings(...)
|
|
(
|
|
isinstance(node.func, ast.Attribute)
|
|
and isinstance(node.func.value, ast.Name)
|
|
and node.func.value.id == "self"
|
|
and node.func.attr == "settings"
|
|
)
|
|
):
|
|
for keyword in node.keywords:
|
|
if keyword.arg == "SENTRY_OPTIONS":
|
|
self.errors.append((keyword.lineno, keyword.col_offset, S011_msg))
|
|
|
|
self.generic_visit(node)
|
|
|
|
|
|
class SentryCheck:
|
|
def __init__(self, tree: ast.AST, filename: str) -> None:
|
|
self.tree = tree
|
|
self.filename = filename
|
|
|
|
def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]:
|
|
visitor = SentryVisitor(self.filename)
|
|
visitor.visit(self.tree)
|
|
|
|
for e in visitor.errors:
|
|
yield (*e, type(self))
|