flake8_plugin.py 5.0 KB

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