diff -r 16f9875e278b -r e8882d16384c UtilitiesPython2/py2flakes/checker.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UtilitiesPython2/py2flakes/checker.py Mon Jan 03 17:10:45 2011 +0100 @@ -0,0 +1,535 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 Detlev Offenbach <detlev@die-offenbachs.de> +# +# Original (c) 2005-2008 Divmod, Inc. +# +# This module is based on pyflakes for Python2 but was heavily hacked to +# work within eric5 + +import __builtin__ +import os.path +import compiler +from compiler import ast + +from py2flakes import messages + +class Binding(object): + """ + Represents the binding of a value to a name. + + The checker uses this to keep track of which names have been bound and + which names have not. See L{Assignment} for a special type of binding that + is checked with stricter rules. + """ + def __init__(self, name, source): + self.name = name + self.source = source + self.used = False + + def __str__(self): + return self.name + + def __repr__(self): + return '<%s object %r from line %r at 0x%x>' % ( + self.__class__.__name__, + self.name, + self.source.lineno, + id(self)) + +class UnBinding(Binding): + ''' + Created by the 'del' operator. + ''' + +class Importation(Binding): + """ + A binding created by an import statement. + """ + def __init__(self, name, source): + self.fullName = name + name = name.split('.')[0] + super(Importation, self).__init__(name, source) + +class Argument(Binding): + """ + Represents binding a name as an argument. + """ + +class Assignment(Binding): + """ + Represents binding a name with an explicit assignment. + + The checker will raise warnings for any Assignment that isn't used. Also, + the checker does not consider assignments in tuple/list unpacking to be + Assignments, rather it treats them as simple Bindings. + """ + +class FunctionDefinition(Binding): + """ + Represents a function definition. + """ + pass + +class ExportBinding(Binding): + """ + A binding created by an __all__ assignment. If the names in the list + can be determined statically, they will be treated as names for export and + additional checking applied to them. + + The only __all__ assignment that can be recognized is one which takes + the value of a literal list containing literal strings. For example:: + + __all__ = ["foo", "bar"] + + Names which are imported and not otherwise used but appear in the value of + __all__ will not have an unused import warning reported for them. + """ + def names(self): + """ + Return a list of the names referenced by this binding. + """ + names = [] + if isinstance(self.source, ast.List): + for node in self.source.nodes: + if isinstance(node, ast.Const): + names.append(node.value) + return names + +class Scope(dict): + """ + Class defining the scope base class. + """ + importStarred = False # set to True when import * is found + + def __repr__(self): + return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), dict.__repr__(self)) + + def __init__(self): + super(Scope, self).__init__() + +class ClassScope(Scope): + """ + Class representing a name scope for a class. + """ + pass + +class FunctionScope(Scope): + """ + Class representing a name scope for a function. + """ + def __init__(self): + super(FunctionScope, self).__init__() + self.globals = {} + +class ModuleScope(Scope): + """ + Class representing a name scope for a module. + """ + pass + +# Globally defined names which are not attributes of the __builtin__ module. +_MAGIC_GLOBALS = ['__file__', '__builtins__'] + +class Checker(object): + """ + Class to check the cleanliness and sanity of Python code. + """ + nodeDepth = 0 + traceTree = False + + def __init__(self, module, filename='(none)'): + """ + Constructor + + @param module parsed module tree or module source code + @param filename name of the module file (string) + """ + self._deferredFunctions = [] + self._deferredAssignments = [] + self.dead_scopes = [] + self.messages = [] + self.filename = filename + self.scopeStack = [ModuleScope()] + self.futuresAllowed = True + + if isinstance(module, str): + module = compiler.parse(module) + self.handleChildren(module) + self._runDeferred(self._deferredFunctions) + # Set _deferredFunctions to None so that deferFunction will fail + # noisily if called after we've run through the deferred functions. + self._deferredFunctions = None + self._runDeferred(self._deferredAssignments) + # Set _deferredAssignments to None so that deferAssignment will fail + # noisly if called after we've run through the deferred assignments. + self._deferredAssignments = None + del self.scopeStack[1:] + self.popScope() + self.check_dead_scopes() + + def deferFunction(self, callable): + ''' + Schedule a function handler to be called just before completion. + + This is used for handling function bodies, which must be deferred + because code later in the file might modify the global scope. When + `callable` is called, the scope at the time this is called will be + restored, however it will contain any new bindings added to it. + ''' + self._deferredFunctions.append((callable, self.scopeStack[:])) + + def deferAssignment(self, callable): + """ + Schedule an assignment handler to be called just after deferred + function handlers. + """ + self._deferredAssignments.append((callable, self.scopeStack[:])) + + def _runDeferred(self, deferred): + """ + Run the callables in C{deferred} using their associated scope stack. + """ + for handler, scope in deferred: + self.scopeStack = scope + handler() + + def scope(self): + return self.scopeStack[-1] + scope = property(scope) + + def popScope(self): + self.dead_scopes.append(self.scopeStack.pop()) + + def check_dead_scopes(self): + """ + Look at scopes which have been fully examined and report names in them + which were imported but unused. + """ + for scope in self.dead_scopes: + export = isinstance(scope.get('__all__'), ExportBinding) + if export: + all = scope['__all__'].names() + if os.path.split(self.filename)[1] != '__init__.py': + # Look for possible mistakes in the export list + undefined = set(all) - set(scope) + for name in undefined: + self.report( + messages.UndefinedExport, + scope['__all__'].source.lineno, + name) + else: + all = [] + + # Look for imported names that aren't used. + for importation in scope.itervalues(): + if isinstance(importation, Importation): + if not importation.used and importation.name not in all: + self.report( + messages.UnusedImport, + importation.source.lineno, + importation.name) + + def pushFunctionScope(self): + self.scopeStack.append(FunctionScope()) + + def pushClassScope(self): + self.scopeStack.append(ClassScope()) + + def report(self, messageClass, *args, **kwargs): + self.messages.append(messageClass(self.filename, *args, **kwargs)) + + def handleChildren(self, tree): + for node in tree.getChildNodes(): + self.handleNode(node, tree) + + def handleNode(self, node, parent): + node.parent = parent + if self.traceTree: + print ' ' * self.nodeDepth + node.__class__.__name__ + self.nodeDepth += 1 + nodeType = node.__class__.__name__.upper() + if nodeType not in ('STMT', 'FROM'): + self.futuresAllowed = False + try: + handler = getattr(self, nodeType) + handler(node) + finally: + self.nodeDepth -= 1 + if self.traceTree: + print ' ' * self.nodeDepth + 'end ' + node.__class__.__name__ + + def ignore(self, node): + pass + + STMT = PRINT = PRINTNL = TUPLE = LIST = ASSTUPLE = ASSATTR = \ + ASSLIST = GETATTR = SLICE = SLICEOBJ = IF = CALLFUNC = DISCARD = \ + RETURN = ADD = MOD = SUB = NOT = UNARYSUB = INVERT = ASSERT = COMPARE = \ + SUBSCRIPT = AND = OR = TRYEXCEPT = RAISE = YIELD = DICT = LEFTSHIFT = \ + RIGHTSHIFT = KEYWORD = TRYFINALLY = WHILE = EXEC = MUL = DIV = POWER = \ + FLOORDIV = BITAND = BITOR = BITXOR = LISTCOMPFOR = LISTCOMPIF = \ + AUGASSIGN = BACKQUOTE = UNARYADD = GENEXPR = GENEXPRFOR = GENEXPRIF = \ + IFEXP = handleChildren + + CONST = PASS = CONTINUE = BREAK = ELLIPSIS = ignore + + def addBinding(self, lineno, value, reportRedef=True): + ''' + Called when a binding is altered. + + @param lineno line of the statement responsible for the change (integer) + @param value the optional new value, a Binding instance, associated + with the binding; if None, the binding is deleted if it exists + @param reportRedef flag indicating if rebinding while unused will be + reported (boolean) + ''' + if (isinstance(self.scope.get(value.name), FunctionDefinition) + and isinstance(value, FunctionDefinition)): + self.report(messages.RedefinedFunction, + lineno, value.name, self.scope[value.name].source.lineno) + + if not isinstance(self.scope, ClassScope): + for scope in self.scopeStack[::-1]: + existing = scope.get(value.name) + if (isinstance(existing, Importation) + and not existing.used + and (not isinstance(value, Importation) or value.fullName == existing.fullName) + and reportRedef): + + self.report(messages.RedefinedWhileUnused, + lineno, value.name, scope[value.name].source.lineno) + + if isinstance(value, UnBinding): + try: + del self.scope[value.name] + except KeyError: + self.report(messages.UndefinedName, lineno, value.name) + else: + self.scope[value.name] = value + + def WITH(self, node): + """ + Handle 'with' by checking the target of the statement (which can be an + identifier, a list or tuple of targets, an attribute, etc) for + undefined names and defining any it adds to the scope and by continuing + to process the suite within the statement. + """ + # Check the "foo" part of a "with foo as bar" statement. Do this no + # matter what, since there's always a "foo" part. + self.handleNode(node.expr, node) + + if node.vars is not None: + self.handleNode(node.vars, node) + + self.handleChildren(node.body) + + + def GLOBAL(self, node): + """ + Keep track of globals declarations. + """ + if isinstance(self.scope, FunctionScope): + self.scope.globals.update(dict.fromkeys(node.names)) + + def LISTCOMP(self, node): + for qual in node.quals: + self.handleNode(qual, node) + self.handleNode(node.expr, node) + + GENEXPRINNER = LISTCOMP + + def FOR(self, node): + """ + Process bindings for loop variables. + """ + vars = [] + def collectLoopVars(n): + if hasattr(n, 'name'): + vars.append(n.name) + else: + for c in n.getChildNodes(): + collectLoopVars(c) + + collectLoopVars(node.assign) + for varn in vars: + if (isinstance(self.scope.get(varn), Importation) + # unused ones will get an unused import warning + and self.scope[varn].used): + self.report(messages.ImportShadowedByLoopVar, + node.lineno, varn, self.scope[varn].source.lineno) + + self.handleChildren(node) + + def NAME(self, node): + """ + Locate the name in locals / function / globals scopes. + """ + # try local scope + importStarred = self.scope.importStarred + try: + self.scope[node.name].used = (self.scope, node.lineno) + except KeyError: + pass + else: + return + + # try enclosing function scopes + + for scope in self.scopeStack[-2:0:-1]: + importStarred = importStarred or scope.importStarred + if not isinstance(scope, FunctionScope): + continue + try: + scope[node.name].used = (self.scope, node.lineno) + except KeyError: + pass + else: + return + + # try global scope + + importStarred = importStarred or self.scopeStack[0].importStarred + try: + self.scopeStack[0][node.name].used = (self.scope, node.lineno) + except KeyError: + if ((not hasattr(__builtin__, node.name)) + and node.name not in _MAGIC_GLOBALS + and not importStarred): + if (os.path.basename(self.filename) == '__init__.py' and + node.name == '__path__'): + # the special name __path__ is valid only in packages + pass + else: + self.report(messages.UndefinedName, node.lineno, node.name) + + + def FUNCTION(self, node): + if getattr(node, "decorators", None) is not None: + self.handleChildren(node.decorators) + self.addBinding(node.lineno, FunctionDefinition(node.name, node)) + self.LAMBDA(node) + + def LAMBDA(self, node): + for default in node.defaults: + self.handleNode(default, node) + + def runFunction(): + args = [] + + def addArgs(arglist): + for arg in arglist: + if isinstance(arg, tuple): + addArgs(arg) + else: + if arg in args: + self.report(messages.DuplicateArgument, node.lineno, arg) + args.append(arg) + + self.pushFunctionScope() + addArgs(node.argnames) + for name in args: + self.addBinding(node.lineno, Argument(name, node), reportRedef=False) + self.handleNode(node.code, node) + def checkUnusedAssignments(): + """ + Check to see if any assignments have not been used. + """ + for name, binding in self.scope.iteritems(): + if (not binding.used and not name in self.scope.globals + and isinstance(binding, Assignment)): + self.report(messages.UnusedVariable, + binding.source.lineno, name) + self.deferAssignment(checkUnusedAssignments) + self.popScope() + + self.deferFunction(runFunction) + + + def CLASS(self, node): + """ + Check names used in a class definition, including its decorators, base + classes, and the body of its definition. Additionally, add its name to + the current scope. + """ + if getattr(node, "decorators", None) is not None: + self.handleChildren(node.decorators) + for baseNode in node.bases: + self.handleNode(baseNode, node) + self.addBinding(node.lineno, Binding(node.name, node)) + self.pushClassScope() + self.handleChildren(node.code) + self.popScope() + + + def ASSNAME(self, node): + if node.flags == 'OP_DELETE': + if isinstance(self.scope, FunctionScope) and node.name in self.scope.globals: + del self.scope.globals[node.name] + else: + self.addBinding(node.lineno, UnBinding(node.name, node)) + else: + # if the name hasn't already been defined in the current scope + if isinstance(self.scope, FunctionScope) and node.name not in self.scope: + # for each function or module scope above us + for scope in self.scopeStack[:-1]: + if not isinstance(scope, (FunctionScope, ModuleScope)): + continue + # if the name was defined in that scope, and the name has + # been accessed already in the current scope, and hasn't + # been declared global + if (node.name in scope + and scope[node.name].used + and scope[node.name].used[0] is self.scope + and node.name not in self.scope.globals): + # then it's probably a mistake + self.report(messages.UndefinedLocal, + scope[node.name].used[1], + node.name, + scope[node.name].source.lineno) + break + + if isinstance(node.parent, + (ast.For, ast.ListCompFor, ast.GenExprFor, + ast.AssTuple, ast.AssList)): + binding = Binding(node.name, node) + elif (node.name == '__all__' and + isinstance(self.scope, ModuleScope) and + isinstance(node.parent, ast.Assign)): + binding = ExportBinding(node.name, node.parent.expr) + else: + binding = Assignment(node.name, node) + if node.name in self.scope: + binding.used = self.scope[node.name].used + self.addBinding(node.lineno, binding) + + def ASSIGN(self, node): + self.handleNode(node.expr, node) + for subnode in node.nodes[::-1]: + self.handleNode(subnode, node) + + def IMPORT(self, node): + for name, alias in node.names: + name = alias or name + importation = Importation(name, node) + self.addBinding(node.lineno, importation) + + def FROM(self, node): + if node.modname == '__future__': + if not self.futuresAllowed: + self.report(messages.LateFutureImport, node.lineno, [n[0] for n in node.names]) + else: + self.futuresAllowed = False + + for name, alias in node.names: + if name == '*': + self.scope.importStarred = True + self.report(messages.ImportStarUsed, node.lineno, node.modname) + continue + name = alias or name + importation = Importation(name, node) + if node.modname == '__future__': + importation.used = (self.scope, node.lineno) + self.addBinding(node.lineno, importation) + +# +# eflag: FileType = Python2