flake8_plugin.py 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  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 use unittest.mock instead of exam.patcher"
  17. S007_msg = "S007 use pytest.fixture(autouse=True) instead of exam.before / exam.around"
  18. class SentryVisitor(ast.NodeVisitor):
  19. def __init__(self, filename: str) -> None:
  20. self.errors: list[tuple[int, int, str]] = []
  21. self.filename = filename
  22. def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
  23. if node.module and not node.level:
  24. if node.module.split(".")[0] in S003_modules:
  25. self.errors.append((node.lineno, node.col_offset, S003_msg))
  26. # for now only enforce this in getsentry
  27. elif (
  28. "getsentry/" in self.filename
  29. and node.module == "sentry.models"
  30. and any(x.name.isupper() or x.name.istitle() for x in node.names)
  31. ):
  32. self.errors.append((node.lineno, node.col_offset, S005_msg))
  33. elif node.module == "exam" and any(x.name == "patcher" for x in node.names):
  34. self.errors.append((node.lineno, node.col_offset, S006_msg))
  35. elif node.module == "exam" and any(x.name in {"before", "around"} for x in node.names):
  36. self.errors.append((node.lineno, node.col_offset, S007_msg))
  37. self.generic_visit(node)
  38. def visit_Import(self, node: ast.Import) -> None:
  39. for alias in node.names:
  40. if alias.name.split(".")[0] in S003_modules:
  41. self.errors.append((node.lineno, node.col_offset, S003_msg))
  42. self.generic_visit(node)
  43. def visit_Attribute(self, node: ast.Attribute) -> None:
  44. if node.attr in S001_methods:
  45. self.errors.append((node.lineno, node.col_offset, S001_fmt.format(node.attr)))
  46. elif node.attr in S004_methods:
  47. self.errors.append((node.lineno, node.col_offset, S004_msg))
  48. self.generic_visit(node)
  49. def visit_Name(self, node: ast.Name) -> None:
  50. if node.id == "print":
  51. self.errors.append((node.lineno, node.col_offset, S002_msg))
  52. self.generic_visit(node)
  53. class SentryCheck:
  54. def __init__(self, tree: ast.AST, filename: str) -> None:
  55. self.tree = tree
  56. self.filename = filename
  57. def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]:
  58. visitor = SentryVisitor(self.filename)
  59. visitor.visit(self.tree)
  60. for e in visitor.errors:
  61. yield (*e, type(self))