check-events 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. #!/usr/bin/env python3
  2. from __future__ import print_function
  3. from pprint import pprint
  4. import argparse
  5. import ast
  6. import os
  7. import sys
  8. SOMETHING = '<something>'
  9. parser = argparse.ArgumentParser()
  10. parser.add_argument('--never-handled',
  11. action='store_true', dest='never_handled',
  12. help='mark never handled events')
  13. parser.add_argument('--no-never-handled',
  14. action='store_false', dest='never_handled',
  15. help='do not mark never handled events')
  16. parser.add_argument('directory', metavar='DIRECTORY',
  17. help='directory to search for .py files')
  18. class Event(object):
  19. def __init__(self, events, name):
  20. self.events = events
  21. self.name = name
  22. self.fired = []
  23. self.handled = []
  24. def fire(self, filename, lineno):
  25. self.fired.append((filename, lineno))
  26. def handle(self, filename, lineno):
  27. self.handled.append((filename, lineno))
  28. def print_summary_one(self, stream, attr, colour, never=True):
  29. lines = getattr(self, attr)
  30. if lines:
  31. for filename, lineno in lines:
  32. stream.write(' \033[{}m{}\033[0m {} +{}\n'.format(
  33. colour, attr[0], filename, lineno))
  34. elif never:
  35. stream.write(' \033[1;33mnever {}\033[0m\n'.format(attr))
  36. def print_summary(self, stream, never_handled):
  37. stream.write('\033[1m{}\033[0m\n'.format(self.name))
  38. self.print_summary_one(stream, 'fired', '1;31')
  39. self.print_summary_one(stream, 'handled', '1;32', never=never_handled)
  40. class Events(dict):
  41. def __missing__(self, key):
  42. self[key] = Event(self, key)
  43. return self[key]
  44. class EventVisitor(ast.NodeVisitor):
  45. def __init__(self, events, filename, *args, **kwargs):
  46. super(EventVisitor, self).__init__(*args, **kwargs)
  47. self.events = events
  48. self.filename = filename
  49. def resolve_attr(self, node):
  50. if isinstance(node, ast.Name):
  51. return node.id
  52. if isinstance(node, ast.Attribute):
  53. return '{}.{}'.format(self.resolve_attr(node.value), node.attr)
  54. raise TypeError('resolve_attr() does not support {!r}'.format(node))
  55. def visit_Call(self, node):
  56. try:
  57. name = self.resolve_attr(node.func)
  58. except TypeError:
  59. # name got something else than identifier in the attribute path;
  60. # this may have been 'str'.format() for example; we can't call
  61. # events this way
  62. return
  63. if name.endswith('.fire_event') or name.endswith('.fire_event_async'):
  64. # here we throw events; event name is the first argument; sometimes
  65. # it is expressed as 'event-stem:' + some_variable
  66. eventnode = node.args[0]
  67. if isinstance(eventnode, ast.Str):
  68. event = eventnode.s
  69. elif isinstance(eventnode, ast.BinOp) \
  70. and isinstance(eventnode.left, ast.Str):
  71. event = eventnode.left.s
  72. else:
  73. raise AssertionError('fishy event {!r} in {} +{}'.format(
  74. eventnode, self.filename, node.lineno))
  75. if ':' in event:
  76. event = ':'.join((event.split(':', 1)[0], SOMETHING))
  77. self.events[event].fire(self.filename, node.lineno)
  78. return
  79. if name in ('qubes.events.handler', 'qubes.ext.handler'):
  80. # here we handle; event names (there may be more than one) are all
  81. # positional arguments
  82. if any(isinstance(arg, ast.Starred) for arg in node.args):
  83. raise AssertionError(
  84. 'event handler with *args in {} +{}'.format(
  85. self.filename, node.lineno))
  86. for arg in node.args:
  87. if not isinstance(arg, ast.Str):
  88. raise AssertionError(
  89. 'event handler with non-string arg in {} +{}'.format(
  90. self.filename, node.lineno))
  91. event = arg.s
  92. if ':' in event:
  93. event = ':'.join((event.split(':', 1)[0], SOMETHING))
  94. self.events[event].handle(self.filename, node.lineno)
  95. return
  96. self.generic_visit(node)
  97. return
  98. def main():
  99. args = parser.parse_args()
  100. events = Events()
  101. for dirpath, dirnames, filenames in os.walk(args.directory):
  102. for filename in filenames:
  103. if not filename.endswith('.py'):
  104. continue
  105. filepath = os.path.join(dirpath, filename)
  106. EventVisitor(events, filepath).visit(
  107. ast.parse(open(filepath).read(), filepath))
  108. for event in sorted(events):
  109. events[event].print_summary(
  110. sys.stdout, never_handled=args.never_handled)
  111. if __name__ == '__main__':
  112. main()