flake8_plugin.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import ast
  2. from collections import namedtuple
  3. from functools import partial
  4. class SentryVisitor(ast.NodeVisitor):
  5. NODE_WINDOW_SIZE = 4
  6. def __init__(self):
  7. self.errors = []
  8. def visit_ImportFrom(self, node):
  9. if node.module in S003.modules:
  10. for nameproxy in node.names:
  11. if nameproxy.name in S003.names:
  12. self.errors.append(S003(node.lineno, node.col_offset))
  13. break
  14. def visit_Import(self, node):
  15. for alias in node.names:
  16. if alias.name.split(".", 1)[0] in S003.modules:
  17. self.errors.append(S003(node.lineno, node.col_offset))
  18. def visit_Call(self, node):
  19. if isinstance(node.func, ast.Attribute):
  20. for bug in (S004,):
  21. if node.func.attr in bug.methods:
  22. call_path = ".".join(self.compose_call_path(node.func.value))
  23. if call_path in bug.invalid_paths:
  24. self.errors.append(bug(node.lineno, node.col_offset))
  25. break
  26. self.generic_visit(node)
  27. def visit_Attribute(self, node):
  28. if node.attr in S001.methods:
  29. self.errors.append(S001(node.lineno, node.col_offset, vars=(node.attr,)))
  30. def visit_Name(self, node):
  31. if node.id == "print":
  32. self.check_print(node)
  33. def visit_Print(self, node):
  34. self.check_print(node)
  35. def check_print(self, node):
  36. self.errors.append(S002(lineno=node.lineno, col=node.col_offset))
  37. def compose_call_path(self, node):
  38. if isinstance(node, ast.Attribute):
  39. yield from self.compose_call_path(node.value)
  40. yield node.attr
  41. elif isinstance(node, ast.Name):
  42. yield node.id
  43. class SentryCheck:
  44. name = "sentry-flake8"
  45. version = "0"
  46. def __init__(self, tree: ast.AST) -> None:
  47. self.tree = tree
  48. def run(self):
  49. visitor = SentryVisitor()
  50. visitor.visit(self.tree)
  51. for e in visitor.errors:
  52. yield self.adapt_error(e)
  53. @classmethod
  54. def adapt_error(cls, e):
  55. """Adapts the extended error namedtuple to be compatible with Flake8."""
  56. return e._replace(message=e.message.format(*e.vars))[:4]
  57. error = namedtuple("error", "lineno col message type vars")
  58. Error = partial(partial, error, message="", type=SentryCheck, vars=())
  59. S001 = Error(
  60. message="S001: Avoid using the {} mock call as it is "
  61. "confusing and prone to causing invalid test "
  62. "behavior."
  63. )
  64. S001.methods = {
  65. "not_called",
  66. "called_once",
  67. "called_once_with",
  68. }
  69. S002 = Error(message="S002: print functions or statements are not allowed.")
  70. S003 = Error(message="S003: Use ``from sentry.utils import json`` instead.")
  71. S003.modules = {"json", "simplejson"}
  72. S003.names = {
  73. "load",
  74. "loads",
  75. "dump",
  76. "dumps",
  77. "JSONEncoder",
  78. "JSONDecodeError",
  79. "_default_encoder",
  80. }
  81. S004 = Error(
  82. message="S004: ``cgi.escape`` and ``html.escape`` should not be used. Use "
  83. "sentry.utils.html.escape instead."
  84. )
  85. S004.methods = {"escape"}
  86. S004.invalid_paths = {"cgi", "html"}