definitions.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. #! /usr/bin/python
  2. import ast
  3. import yaml
  4. class DefinitionVisitor(ast.NodeVisitor):
  5. def __init__(self):
  6. super(DefinitionVisitor, self).__init__()
  7. self.functions = {}
  8. self.classes = {}
  9. self.names = {}
  10. self.attrs = set()
  11. self.definitions = {
  12. 'def': self.functions,
  13. 'class': self.classes,
  14. 'names': self.names,
  15. 'attrs': self.attrs,
  16. }
  17. def visit_Name(self, node):
  18. self.names.setdefault(type(node.ctx).__name__, set()).add(node.id)
  19. def visit_Attribute(self, node):
  20. self.attrs.add(node.attr)
  21. for child in ast.iter_child_nodes(node):
  22. self.visit(child)
  23. def visit_ClassDef(self, node):
  24. visitor = DefinitionVisitor()
  25. self.classes[node.name] = visitor.definitions
  26. for child in ast.iter_child_nodes(node):
  27. visitor.visit(child)
  28. def visit_FunctionDef(self, node):
  29. visitor = DefinitionVisitor()
  30. self.functions[node.name] = visitor.definitions
  31. for child in ast.iter_child_nodes(node):
  32. visitor.visit(child)
  33. def non_empty(defs):
  34. functions = {name: non_empty(f) for name, f in defs['def'].items()}
  35. classes = {name: non_empty(f) for name, f in defs['class'].items()}
  36. result = {}
  37. if functions: result['def'] = functions
  38. if classes: result['class'] = classes
  39. names = defs['names']
  40. uses = []
  41. for name in names.get('Load', ()):
  42. if name not in names.get('Param', ()) and name not in names.get('Store', ()):
  43. uses.append(name)
  44. uses.extend(defs['attrs'])
  45. if uses: result['uses'] = uses
  46. result['names'] = names
  47. result['attrs'] = defs['attrs']
  48. return result
  49. def definitions_in_code(input_code):
  50. input_ast = ast.parse(input_code)
  51. visitor = DefinitionVisitor()
  52. visitor.visit(input_ast)
  53. definitions = non_empty(visitor.definitions)
  54. return definitions
  55. def definitions_in_file(filepath):
  56. with open(filepath) as f:
  57. return definitions_in_code(f.read())
  58. def defined_names(prefix, defs, names):
  59. for name, funcs in defs.get('def', {}).items():
  60. names.setdefault(name, {'defined': []})['defined'].append(prefix + name)
  61. defined_names(prefix + name + ".", funcs, names)
  62. for name, funcs in defs.get('class', {}).items():
  63. names.setdefault(name, {'defined': []})['defined'].append(prefix + name)
  64. defined_names(prefix + name + ".", funcs, names)
  65. def used_names(prefix, item, defs, names):
  66. for name, funcs in defs.get('def', {}).items():
  67. used_names(prefix + name + ".", name, funcs, names)
  68. for name, funcs in defs.get('class', {}).items():
  69. used_names(prefix + name + ".", name, funcs, names)
  70. path = prefix.rstrip('.')
  71. for used in defs.get('uses', ()):
  72. if used in names:
  73. if item:
  74. names[item].setdefault('uses', []).append(used)
  75. names[used].setdefault('used', {}).setdefault(item, []).append(path)
  76. if __name__ == '__main__':
  77. import sys, os, argparse, re
  78. parser = argparse.ArgumentParser(description='Find definitions.')
  79. parser.add_argument(
  80. "--unused", action="store_true", help="Only list unused definitions"
  81. )
  82. parser.add_argument(
  83. "--ignore", action="append", metavar="REGEXP", help="Ignore a pattern"
  84. )
  85. parser.add_argument(
  86. "--pattern", action="append", metavar="REGEXP",
  87. help="Search for a pattern"
  88. )
  89. parser.add_argument(
  90. "directories", nargs='+', metavar="DIR",
  91. help="Directories to search for definitions"
  92. )
  93. parser.add_argument(
  94. "--referrers", default=0, type=int,
  95. help="Include referrers up to the given depth"
  96. )
  97. parser.add_argument(
  98. "--referred", default=0, type=int,
  99. help="Include referred down to the given depth"
  100. )
  101. parser.add_argument(
  102. "--format", default="yaml",
  103. help="Output format, one of 'yaml' or 'dot'"
  104. )
  105. args = parser.parse_args()
  106. definitions = {}
  107. for directory in args.directories:
  108. for root, dirs, files in os.walk(directory):
  109. for filename in files:
  110. if filename.endswith(".py"):
  111. filepath = os.path.join(root, filename)
  112. definitions[filepath] = definitions_in_file(filepath)
  113. names = {}
  114. for filepath, defs in definitions.items():
  115. defined_names(filepath + ":", defs, names)
  116. for filepath, defs in definitions.items():
  117. used_names(filepath + ":", None, defs, names)
  118. patterns = [re.compile(pattern) for pattern in args.pattern or ()]
  119. ignore = [re.compile(pattern) for pattern in args.ignore or ()]
  120. result = {}
  121. for name, definition in names.items():
  122. if patterns and not any(pattern.match(name) for pattern in patterns):
  123. continue
  124. if ignore and any(pattern.match(name) for pattern in ignore):
  125. continue
  126. if args.unused and definition.get('used'):
  127. continue
  128. result[name] = definition
  129. referrer_depth = args.referrers
  130. referrers = set()
  131. while referrer_depth:
  132. referrer_depth -= 1
  133. for entry in result.values():
  134. for used_by in entry.get("used", ()):
  135. referrers.add(used_by)
  136. for name, definition in names.items():
  137. if not name in referrers:
  138. continue
  139. if ignore and any(pattern.match(name) for pattern in ignore):
  140. continue
  141. result[name] = definition
  142. referred_depth = args.referred
  143. referred = set()
  144. while referred_depth:
  145. referred_depth -= 1
  146. for entry in result.values():
  147. for uses in entry.get("uses", ()):
  148. referred.add(uses)
  149. for name, definition in names.items():
  150. if not name in referred:
  151. continue
  152. if ignore and any(pattern.match(name) for pattern in ignore):
  153. continue
  154. result[name] = definition
  155. if args.format == 'yaml':
  156. yaml.dump(result, sys.stdout, default_flow_style=False)
  157. elif args.format == 'dot':
  158. print "digraph {"
  159. for name, entry in result.items():
  160. print name
  161. for used_by in entry.get("used", ()):
  162. if used_by in result:
  163. print used_by, "->", name
  164. print "}"
  165. else:
  166. raise ValueError("Unknown format %r" % (args.format))