flake8_plugin.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. from __future__ import annotations
  2. import ast
  3. from collections.abc import Generator
  4. from typing import Any
  5. S001_fmt = (
  6. "S001 Avoid using the {} mock call as it is "
  7. "confusing and prone to causing invalid test "
  8. "behavior."
  9. )
  10. S001_methods = frozenset(("not_called", "called_once", "called_once_with"))
  11. S002_msg = "S002 print functions or statements are not allowed."
  12. S003_msg = "S003 Use ``from sentry.utils import json`` instead."
  13. S003_modules = frozenset(("json", "simplejson"))
  14. S004_msg = "S004 Use `pytest.raises` instead for better debuggability."
  15. S004_methods = frozenset(("assertRaises", "assertRaisesRegex"))
  16. S005_msg = "S005 Do not import models from sentry.models but the actual module"
  17. S006_msg = "S006 Do not use force_bytes / force_str -- test the types directly"
  18. S007_msg = "S007 Do not import sentry.testutils into production code."
  19. S008_msg = "S008 Use datetime.fromisoformat rather than guessing at date formats"
  20. S009_msg = "S009 Use `raise` with no arguments to reraise exceptions"
  21. S010_msg = "S010 Except handler does nothing and should be removed"
  22. S011_msg = "S011 Use override_options(...) instead to ensure proper cleanup"
  23. # SentryIsAuthenticated extends from IsAuthenticated and provides additional checks for demo users
  24. S012_msg = "S012 Use ``from sentry.api.permissions import SentryIsAuthenticated`` instead"
  25. class SentryVisitor(ast.NodeVisitor):
  26. def __init__(self, filename: str) -> None:
  27. self.errors: list[tuple[int, int, str]] = []
  28. self.filename = filename
  29. self._except_vars: list[str | None] = []
  30. def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
  31. if node.module and not node.level:
  32. if node.module.split(".")[0] in S003_modules:
  33. self.errors.append((node.lineno, node.col_offset, S003_msg))
  34. elif node.module == "sentry.models":
  35. self.errors.append((node.lineno, node.col_offset, S005_msg))
  36. elif (
  37. ("tests/" in self.filename or "testutils/" in self.filename)
  38. and node.module == "django.utils.encoding"
  39. and any(x.name in {"force_bytes", "force_str"} for x in node.names)
  40. ):
  41. self.errors.append((node.lineno, node.col_offset, S006_msg))
  42. elif (
  43. "tests/" in self.filename or "testutils/" in self.filename
  44. ) and node.module == "dateutil.parser":
  45. self.errors.append((node.lineno, node.col_offset, S008_msg))
  46. elif (
  47. "tests/" not in self.filename
  48. and "fixtures/" not in self.filename
  49. and "sentry/testutils/" not in self.filename
  50. and "sentry.testutils" in node.module
  51. ):
  52. self.errors.append((node.lineno, node.col_offset, S007_msg))
  53. elif node.module == "rest_framework.permissions" and any(
  54. x.name == "IsAuthenticated" for x in node.names
  55. ):
  56. self.errors.append((node.lineno, node.col_offset, S012_msg))
  57. self.generic_visit(node)
  58. def visit_Import(self, node: ast.Import) -> None:
  59. for alias in node.names:
  60. if alias.name.split(".")[0] in S003_modules:
  61. self.errors.append((node.lineno, node.col_offset, S003_msg))
  62. elif (
  63. "tests/" not in self.filename
  64. and "fixtures/" not in self.filename
  65. and "sentry/testutils/" not in self.filename
  66. and "sentry.testutils" in alias.name
  67. ):
  68. self.errors.append((node.lineno, node.col_offset, S007_msg))
  69. self.generic_visit(node)
  70. def visit_Attribute(self, node: ast.Attribute) -> None:
  71. if node.attr in S001_methods:
  72. self.errors.append((node.lineno, node.col_offset, S001_fmt.format(node.attr)))
  73. elif node.attr in S004_methods:
  74. self.errors.append((node.lineno, node.col_offset, S004_msg))
  75. self.generic_visit(node)
  76. def visit_Name(self, node: ast.Name) -> None:
  77. if node.id == "print":
  78. self.errors.append((node.lineno, node.col_offset, S002_msg))
  79. self.generic_visit(node)
  80. def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
  81. self._except_vars.append(node.name)
  82. try:
  83. self.generic_visit(node)
  84. finally:
  85. self._except_vars.pop()
  86. def visit_Raise(self, node: ast.Raise) -> None:
  87. if (
  88. self._except_vars
  89. and isinstance(node.exc, ast.Name)
  90. and node.exc.id == self._except_vars[-1]
  91. ):
  92. self.errors.append((node.lineno, node.col_offset, S009_msg))
  93. self.generic_visit(node)
  94. def visit_Try(self, node: ast.Try) -> None:
  95. if (
  96. node.handlers
  97. and len(node.handlers[-1].body) == 1
  98. and isinstance(node.handlers[-1].body[0], ast.Raise)
  99. and node.handlers[-1].body[0].exc is None
  100. ):
  101. self.errors.append((node.handlers[-1].lineno, node.handlers[-1].col_offset, S010_msg))
  102. self.generic_visit(node)
  103. def visit_Call(self, node: ast.Call) -> None:
  104. if (
  105. # override_settings(...)
  106. (isinstance(node.func, ast.Name) and node.func.id == "override_settings")
  107. or
  108. # self.settings(...)
  109. (
  110. isinstance(node.func, ast.Attribute)
  111. and isinstance(node.func.value, ast.Name)
  112. and node.func.value.id == "self"
  113. and node.func.attr == "settings"
  114. )
  115. ):
  116. for keyword in node.keywords:
  117. if keyword.arg == "SENTRY_OPTIONS":
  118. self.errors.append((keyword.lineno, keyword.col_offset, S011_msg))
  119. self.generic_visit(node)
  120. class SentryCheck:
  121. def __init__(self, tree: ast.AST, filename: str) -> None:
  122. self.tree = tree
  123. self.filename = filename
  124. def run(self) -> Generator[tuple[int, int, str, type[Any]]]:
  125. visitor = SentryVisitor(self.filename)
  126. visitor.visit(self.tree)
  127. for e in visitor.errors:
  128. yield (*e, type(self))