VultureChecker/vulture.py

changeset 1
ea6aed49cd69
child 7
a1a6ff3e5486
diff -r 9b743f8d436b -r ea6aed49cd69 VultureChecker/vulture.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VultureChecker/vulture.py	Sat Oct 03 19:07:40 2015 +0200
@@ -0,0 +1,289 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# vulture - Find dead code.
+#
+# Copyright (C) 2012-2015  Jendrik Seipp (jendrikseipp@web.de)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Patched to support the Python 3.5 async functionality.
+#
+
+from __future__ import print_function
+
+import ast
+from fnmatch import fnmatchcase
+import optparse
+import os
+import re
+import sys
+
+__version__ = '0.8.1-p1'
+
+# Parse variable names in template strings.
+FORMAT_STRING_PATTERNS = [re.compile(r'\%\((\w+)\)'), re.compile(r'{(\w+)}')]
+
+
+def _ignore_function(name):
+    return ((name.startswith('__') and name.endswith('__')) or
+            name.startswith('test_'))
+
+
+class Item(str):
+    def __new__(cls, name, typ, file, lineno):
+        item = str.__new__(cls, name)
+        item.typ = typ
+        item.file = file
+        item.lineno = lineno
+        return item
+
+
+class Vulture(ast.NodeVisitor):
+    """Find dead stuff."""
+    def __init__(self, exclude=None, verbose=False):
+        self.exclude = []
+        for pattern in exclude or []:
+            if not any(char in pattern for char in ['*', '?', '[']):
+                pattern = '*%s*' % pattern
+            self.exclude.append(pattern)
+
+        self.verbose = verbose
+
+        self.defined_attrs = []
+        self.defined_funcs = []
+        self.defined_props = []
+        self.defined_vars = []
+        self.used_attrs = []
+        self.used_vars = []
+        self.tuple_assign_vars = []
+        self.names_imported_as_aliases = []
+
+        self.file = ''
+        self.code = None
+
+    def scan(self, node_string):
+        self.code = node_string.splitlines()
+        node = ast.parse(node_string, filename=self.file)
+        self.visit(node)
+
+    def _get_modules(self, paths, toplevel=True):
+        """Take files from the command line even if they don't end with .py."""
+        modules = []
+        for path in paths:
+            path = os.path.abspath(path)
+            if os.path.isfile(path) and (path.endswith('.py') or toplevel):
+                modules.append(path)
+            elif os.path.isdir(path):
+                subpaths = [os.path.join(path, filename)
+                            for filename in sorted(os.listdir(path))]
+                modules.extend(self._get_modules(subpaths, toplevel=False))
+            elif toplevel:
+                sys.exit('Error: %s could not be found.' % path)
+        return modules
+
+    def scavenge(self, paths):
+        modules = self._get_modules(paths)
+        included_modules = []
+        for module in modules:
+            if any(fnmatchcase(module, pattern) for pattern in self.exclude):
+                self.log('Excluded:', module)
+            else:
+                included_modules.append(module)
+
+        for module in included_modules:
+            self.log('Scanning:', module)
+            with open(module) as f:
+                module_string = f.read()
+            self.file = module
+            self.scan(module_string)
+
+    def report(self):
+        def file_lineno(item):
+            return (item.file.lower(), item.lineno)
+        unused_item_found = False
+        for item in sorted(self.unused_funcs + self.unused_props +
+                           self.unused_vars + self.unused_attrs,
+                           key=file_lineno):
+            relpath = os.path.relpath(item.file)
+            path = relpath if not relpath.startswith('..') else item.file
+            print("%s:%d: Unused %s '%s'" % (path, item.lineno, item.typ,
+                                             item))
+            unused_item_found = True
+        return unused_item_found
+
+    def get_unused(self, defined, used):
+        return list(sorted(set(defined) - set(used), key=lambda x: x.lower()))
+
+    @property
+    def unused_funcs(self):
+        return self.get_unused(
+            self.defined_funcs,
+            self.used_attrs + self.used_vars + self.names_imported_as_aliases)
+
+    @property
+    def unused_props(self):
+        return self.get_unused(self.defined_props, self.used_attrs)
+
+    @property
+    def unused_vars(self):
+        return self.get_unused(
+            self.defined_vars,
+            self.used_attrs + self.used_vars + self.tuple_assign_vars +
+            self.names_imported_as_aliases)
+
+    @property
+    def unused_attrs(self):
+        return self.get_unused(self.defined_attrs, self.used_attrs)
+
+    def _get_lineno(self, node):
+        return getattr(node, 'lineno', 1)
+
+    def _get_line(self, node):
+        return self.code[self._get_lineno(node) - 1] if self.code else ''
+
+    def _get_item(self, node, typ):
+        name = getattr(node, 'name', None)
+        id = getattr(node, 'id', None)
+        attr = getattr(node, 'attr', None)
+        assert len([x for x in (name, id, attr) if x is not None]) == 1
+        return Item(name or id or attr, typ, self.file, node.lineno)
+
+    def log(self, *args):
+        if self.verbose:
+            print(*args)
+
+    def print_node(self, node):
+        # Only create the strings, if we'll also print them.
+        if self.verbose:
+            self.log(
+                self._get_lineno(node), ast.dump(node), self._get_line(node))
+
+    def visit_FunctionDef(self, node):
+        for decorator in node.decorator_list:
+            if getattr(decorator, 'id', None) == 'property':
+                self.defined_props.append(self._get_item(node, 'property'))
+                break
+        else:
+            # Function is not a property.
+            if not _ignore_function(node.name):
+                self.defined_funcs.append(self._get_item(node, 'function'))
+
+    visit_AsyncFunctionDef = visit_FunctionDef
+
+    def visit_Attribute(self, node):
+        item = self._get_item(node, 'attribute')
+        if isinstance(node.ctx, ast.Store):
+            self.log('defined_attrs <-', item)
+            self.defined_attrs.append(item)
+        elif isinstance(node.ctx, ast.Load):
+            self.log('used_attrs <-', item)
+            self.used_attrs.append(item)
+
+    def visit_Name(self, node):
+        if node.id != 'object':
+            if isinstance(node.ctx, ast.Load):
+                self.log('used_vars <-', node.id)
+                self.used_vars.append(node.id)
+            elif isinstance(node.ctx, ast.Store):
+                # Ignore _x (pylint convention), __x, __x__ (special method).
+                if not node.id.startswith('_'):
+                    item = self._get_item(node, 'variable')
+                    self.log('defined_vars <-', item)
+                    self.defined_vars.append(item)
+
+    def visit_Import(self, node):
+        self._add_aliases(node)
+
+    def visit_ImportFrom(self, node):
+        self._add_aliases(node)
+
+    def _add_aliases(self, node):
+        assert isinstance(node, (ast.Import, ast.ImportFrom))
+        for name_and_alias in node.names:
+            alias = name_and_alias.asname
+            if alias is not None:
+                name = name_and_alias.name
+                self.log('names_imported_as_aliases <- %s' % name)
+                self.names_imported_as_aliases.append(name)
+
+    def _find_tuple_assigns(self, node):
+        # Find all tuple assignments. Those have the form
+        # Assign->Tuple->Name or For->Tuple->Name or comprehension->Tuple->Name
+        for child in ast.iter_child_nodes(node):
+            if not isinstance(child, ast.Tuple):
+                continue
+            for grandchild in ast.walk(child):
+                if (isinstance(grandchild, ast.Name) and
+                        isinstance(grandchild.ctx, ast.Store)):
+                        self.log('tuple_assign_vars <-', grandchild.id)
+                        self.tuple_assign_vars.append(grandchild.id)
+
+    def visit_Assign(self, node):
+        self._find_tuple_assigns(node)
+
+    def visit_For(self, node):
+        self._find_tuple_assigns(node)
+
+    visit_AsyncFor = visit_For
+
+    def visit_comprehension(self, node):
+        self._find_tuple_assigns(node)
+
+    def visit_ClassDef(self, node):
+        self.defined_funcs.append(self._get_item(node, 'class'))
+
+    def visit_Str(self, node):
+        """
+        Variables may appear in format strings:
+
+        '%(my_var)s' % locals()
+        '{my_var}'.format(**locals())
+
+        """
+        for pattern in FORMAT_STRING_PATTERNS:
+            self.used_vars.extend(pattern.findall(node.s))
+
+    def visit(self, node):
+        method = 'visit_' + node.__class__.__name__
+        visitor = getattr(self, method, None)
+        if visitor is not None:
+            self.print_node(node)
+            visitor(node)
+        return self.generic_visit(node)
+
+
+def parse_args():
+    def csv(option, opt, value, parser):
+        setattr(parser.values, option.dest, value.split(','))
+    usage = 'usage: %prog [options] PATH [PATH ...]'
+    parser = optparse.OptionParser(usage=usage)
+    parser.add_option('--exclude', action='callback', callback=csv,
+                      type='string', default=[],
+                      help='Comma-separated list of filename patterns to '
+                           'exclude (e.g. svn,external).')
+    parser.add_option('-v', '--verbose', action='store_true')
+    options, args = parser.parse_args()
+    return options, args
+
+
+def main():
+    options, args = parse_args()
+    vulture = Vulture(exclude=options.exclude, verbose=options.verbose)
+    vulture.scavenge(args)
+    sys.exit(vulture.report())
+
+
+if __name__ == '__main__':
+    main()

eric ide

mercurial