diff -r 86047f5f4155 -r 5af61402d74d Plugins/CheckerPlugins/SyntaxChecker/pyflakes/checker.py --- a/Plugins/CheckerPlugins/SyntaxChecker/pyflakes/checker.py Sat Jan 04 22:14:38 2014 +0100 +++ b/Plugins/CheckerPlugins/SyntaxChecker/pyflakes/checker.py Sun Jan 05 22:45:29 2014 +0100 @@ -1,34 +1,45 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# Copyright (c) 2010 - 2014 Detlev Offenbach <detlev@die-offenbachs.de> # # Original (c) 2005-2010 Divmod, Inc. # -# This module is based on pyflakes for Python2 but was modified to -# work with eric5 +# This module is based on pyflakes but was modified to work with eric5 +""" +Main module. -import os.path +Implement the central Checker class. +Also, it models the Bindings and Scopes. +""" +import doctest +import os +import sys try: - import builtins + builtin_vars = dir(__import__('builtins')) PY2 = False except ImportError: - import __builtin__ as builtins #__IGNORE_WARNING__ + builtin_vars = dir(__import__('__builtin__')) PY2 = True try: import ast iter_child_nodes = ast.iter_child_nodes -except (ImportError, AttributeError): # Python 2.5 +except ImportError: # Python 2.5 import _ast as ast - def iter_child_nodes(node, astcls=ast.AST): + if 'decorator_list' not in ast.ClassDef._fields: + # Patch the missing attribute 'decorator_list' + ast.ClassDef.decorator_list = () + ast.FunctionDef.decorator_list = property(lambda s: s.decorators) + + def iter_child_nodes(node): """ - Yield all direct child nodes of *node*, that is, all fields that are nodes - and all items of fields that are lists of nodes. + Yield all direct child nodes of *node*, that is, all fields that + are nodes and all items of fields that are lists of nodes. """ for name in node._fields: field = getattr(node, name, None) - if isinstance(field, astcls): + if isinstance(field, ast.AST): yield field elif isinstance(field, list): for item in field: @@ -44,6 +55,15 @@ from . import messages +if PY2: + def getNodeType(node_class): + # workaround str.upper() which is locale-dependent + return str(unicode(node_class.__name__).upper()) # __IGNORE_WARNING__ +else: + def getNodeType(node_class): + return node_class.__name__.upper() + + class Binding(object): """ Represents the binding of a value to a name. @@ -51,6 +71,9 @@ 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. + + @ivar used: pair of (L{Scope}, line-number) indicating the scope and + line number that this binding was last used """ def __init__(self, name, source): @@ -68,13 +91,13 @@ id(self)) -class UnBinding(Binding): - """Created by the 'del' operator.""" - - class Importation(Binding): """ A binding created by an import statement. + + @ivar fullName: The complete name given to the import statement, + possibly including multiple dotted components. + @type fullName: C{str} """ def __init__(self, name, source): self.fullName = name @@ -140,10 +163,10 @@ class Scope(dict): importStarred = False # set to True when import * is found - usesLocals = False def __repr__(self): - return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), dict.__repr__(self)) + scope_cls = self.__class__.__name__ + return '<%s at 0x%x %s>' % (scope_cls, id(self), dict.__repr__(self)) class ClassScope(Scope): @@ -153,10 +176,31 @@ class FunctionScope(Scope): """ I represent a name scope for a function. + + @ivar globals: Names declared 'global' in this function. """ + usesLocals = False + alwaysUsed = set(['__tracebackhide__', + '__traceback_info__', '__traceback_supplement__']) + def __init__(self): super(FunctionScope, self).__init__() - self.globals = {} + # Simplify: manage the special locals as globals + self.globals = self.alwaysUsed.copy() + + def unusedAssignments(self): + """ + Return a generator for the assignments which have not been used. + """ + for name, binding in self.items(): + if (not binding.used and name not in self.globals + and not self.usesLocals + and isinstance(binding, Assignment)): + yield name, binding + + +class GeneratorScope(Scope): + pass class ModuleScope(Scope): @@ -179,13 +223,29 @@ class Checker(object): """ I check the cleanliness and sanity of Python code. + + @ivar _deferredFunctions: Tracking list used by L{deferFunction}. Elements + of the list are two-tuples. The first element is the callable passed + to L{deferFunction}. The second element is a copy of the scope stack + at the time L{deferFunction} was called. + + @ivar _deferredAssignments: Similar to C{_deferredFunctions}, but for + callables which are deferred assignment checks. """ nodeDepth = 0 + offset = None traceTree = False - builtIns = set(dir(builtins)) | set(_MAGIC_GLOBALS) + withDoctest = ('PYFLAKES_NODOCTEST' not in os.environ) + + builtIns = set(builtin_vars).union(_MAGIC_GLOBALS) + _customBuiltIns = os.environ.get('PYFLAKES_BUILTINS') + if _customBuiltIns: + builtIns.update(_customBuiltIns.split(',')) + del _customBuiltIns def __init__(self, tree, filename='(none)', builtins=None): + self._nodeHandlers = {} self._deferredFunctions = [] self._deferredAssignments = [] self.deadScopes = [] @@ -194,6 +254,7 @@ if builtins: self.builtIns = self.builtIns.union(builtins) self.scopeStack = [ModuleScope()] + self.exceptHandlers = [()] self.futuresAllowed = True self.root = tree self.handleChildren(tree) @@ -218,21 +279,22 @@ `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[:])) + self._deferredFunctions.append((callable, self.scopeStack[:], self.offset)) def deferAssignment(self, callable): """ Schedule an assignment handler to be called just after deferred function handlers. """ - self._deferredAssignments.append((callable, self.scopeStack[:])) + self._deferredAssignments.append((callable, self.scopeStack[:], self.offset)) def runDeferred(self, deferred): """ Run the callables in C{deferred} using their associated scope stack. """ - for handler, scope in deferred: + for handler, scope, offset in deferred: self.scopeStack = scope + self.offset = offset handler() @property @@ -251,12 +313,13 @@ export = isinstance(scope.get('__all__'), ExportBinding) if export: all = scope['__all__'].names() - if not scope.importStarred and os.path.basename(self.filename) != '__init__.py': + if not scope.importStarred and \ + os.path.basename(self.filename) != '__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) + scope['__all__'].source, name) else: all = [] @@ -265,13 +328,16 @@ if isinstance(importation, Importation): if not importation.used and importation.name not in all: self.report(messages.UnusedImport, - importation.source.lineno, importation.name) + importation.source, importation.name) + + def pushScope(self, scopeClass=FunctionScope): + self.scopeStack.append(scopeClass()) - def pushFunctionScope(self): - self.scopeStack.append(FunctionScope()) + def pushFunctionScope(self): # XXX Deprecated + self.pushScope(FunctionScope) - def pushClassScope(self): - self.scopeStack.append(ClassScope()) + def pushClassScope(self): # XXX Deprecated + self.pushScope(ClassScope) def report(self, messageClass, *args, **kwargs): self.messages.append(messageClass(self.filename, *args, **kwargs)) @@ -341,69 +407,76 @@ existing = scope.get(value.name) if (isinstance(existing, Importation) and not existing.used - and (not isinstance(value, Importation) or value.fullName == existing.fullName) + and (not isinstance(value, Importation) or + value.fullName == existing.fullName) and reportRedef and not self.differentForks(node, existing.source)): redefinedWhileUnused = True self.report(messages.RedefinedWhileUnused, - node.lineno, value.name, existing.source.lineno) + node, value.name, existing.source) existing = self.scope.get(value.name) if not redefinedWhileUnused and self.hasParent(value.source, ast.ListComp): if (existing and reportRedef - and not self.hasParent(existing.source, (ast.For, ast.ListComp))): + and not self.hasParent(existing.source, (ast.For, ast.ListComp)) + and not self.differentForks(node, existing.source)): self.report(messages.RedefinedInListComp, - node.lineno, value.name, existing.source.lineno) + node, value.name, existing.source) - if isinstance(value, UnBinding): - try: - del self.scope[value.name] - except KeyError: - self.report(messages.UndefinedName, node.lineno, value.name) - elif (isinstance(existing, Definition) - and not existing.used - and not self.differentForks(node, existing.source)): + if (isinstance(existing, Definition) + and not existing.used + and not self.differentForks(node, existing.source)): self.report(messages.RedefinedWhileUnused, - node.lineno, value.name, existing.source.lineno) + node, value.name, existing.source) else: self.scope[value.name] = value + def getNodeHandler(self, node_class): + try: + return self._nodeHandlers[node_class] + except KeyError: + nodeType = getNodeType(node_class) + self._nodeHandlers[node_class] = handler = getattr(self, nodeType) + return handler + def handleNodeLoad(self, node): name = getNodeName(node) if not name: return # try local scope - importStarred = self.scope.importStarred try: - self.scope[name].used = (self.scope, node.lineno) + self.scope[name].used = (self.scope, node) except KeyError: pass else: return - # try enclosing function scopes - for scope in self.scopeStack[-2:0:-1]: + scopes = [scope for scope in self.scopeStack[:-1] + if isinstance(scope, (FunctionScope, ModuleScope))] + if isinstance(self.scope, GeneratorScope) and scopes[-1] != self.scopeStack[-2]: + scopes.append(self.scopeStack[-2]) + + # try enclosing function scopes and global scope + importStarred = self.scope.importStarred + for scope in reversed(scopes): importStarred = importStarred or scope.importStarred - if not isinstance(scope, FunctionScope): - continue try: - scope[name].used = (self.scope, node.lineno) + scope[name].used = (self.scope, node) except KeyError: pass else: return - # try global scope - importStarred = importStarred or self.scopeStack[0].importStarred - try: - self.scopeStack[0][name].used = (self.scope, node.lineno) - except KeyError: - if not importStarred and name not in self.builtIns: - if (os.path.basename(self.filename) == '__init__.py' and name == '__path__'): - # the special name __path__ is valid only in packages - pass - else: - self.report(messages.UndefinedName, node.lineno, name) + # look in the built-ins + if importStarred or name in self.builtIns: + return + if name == '__path__' and os.path.basename(self.filename) == '__init__.py': + # the special name __path__ is valid only in packages + return + + # protected with a NameError handler? + if 'NameError' not in self.exceptHandlers[-1]: + self.report(messages.UndefinedName, node, name) def handleNodeStore(self, node): name = getNodeName(node) @@ -418,17 +491,18 @@ # 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 (name in scope and scope[name].used and scope[name].used[0] is self.scope - and name not in self.scope.globals): + used = name in scope and scope[name].used + if used and used[0] is self.scope and name not in self.scope.globals: # then it's probably a mistake self.report(messages.UndefinedLocal, - scope[name].used[1], name, scope[name].source.lineno) + scope[name].used[1], name, scope[name].source) break parent = getattr(node, 'parent', None) if isinstance(parent, (ast.For, ast.comprehension, ast.Tuple, ast.List)): binding = Binding(name, node) - elif parent is not None and name == '__all__' and isinstance(self.scope, ModuleScope): + elif (parent is not None and name == '__all__' and + isinstance(self.scope, ModuleScope)): binding = ExportBinding(name, parent.value) else: binding = Assignment(name, node) @@ -441,9 +515,12 @@ if not name: return if isinstance(self.scope, FunctionScope) and name in self.scope.globals: - del self.scope.globals[name] + self.scope.globals.remove(name) else: - self.addBinding(node, UnBinding(name, node)) + try: + del self.scope[name] + except KeyError: + self.report(messages.UndefinedName, node, name) def handleChildren(self, tree): for node in iter_child_nodes(tree): @@ -457,32 +534,72 @@ return isinstance(node, ast.Str) or (isinstance(node, ast.Expr) and isinstance(node.value, ast.Str)) + def getDocstring(self, node): + if isinstance(node, ast.Expr): + node = node.value + if not isinstance(node, ast.Str): + return (None, None) + # Computed incorrectly if the docstring has backslash + doctest_lineno = node.lineno - node.s.count('\n') - 1 + return (node.s, doctest_lineno) + def handleNode(self, node, parent): if node is None: return - node.parent = parent + if self.offset and getattr(node, 'lineno', None) is not None: + node.lineno += self.offset[0] + node.col_offset += self.offset[1] if self.traceTree: print(' ' * self.nodeDepth + node.__class__.__name__) - self.nodeDepth += 1 if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or self.isDocstring(node)): self.futuresAllowed = False - nodeType = node.__class__.__name__.upper() + self.nodeDepth += 1 node.level = self.nodeDepth + node.parent = parent try: - handler = getattr(self, nodeType) + handler = self.getNodeHandler(node.__class__) handler(node) finally: self.nodeDepth -= 1 if self.traceTree: print(' ' * self.nodeDepth + 'end ' + node.__class__.__name__) + _getDoctestExamples = doctest.DocTestParser().get_examples + + def handleDoctests(self, node): + try: + docstring, node_lineno = self.getDocstring(node.body[0]) + if not docstring: + return + examples = self._getDoctestExamples(docstring) + except (ValueError, IndexError): + # e.g. line 6 of the docstring for <string> has inconsistent + # leading whitespace: ... + return + node_offset = self.offset or (0, 0) + self.pushScope() + for example in examples: + try: + tree = compile(example.source, "<doctest>", "exec", ast.PyCF_ONLY_AST) + except SyntaxError: + e = sys.exc_info()[1] + position = (node_lineno + example.lineno + e.lineno, + example.indent + 4 + e.offset) + self.report(messages.DoctestSyntaxError, node, position) + else: + self.offset = (node_offset[0] + node_lineno + example.lineno, + node_offset[1] + example.indent + 4) + self.handleChildren(tree) + self.offset = node_offset + self.popScope() + def ignore(self, node): pass # "stmt" type nodes RETURN = DELETE = PRINT = WHILE = IF = WITH = WITHITEM = RAISE = \ - TRYEXCEPT = TRYFINALLY = TRY = ASSERT = EXEC = EXPR = handleChildren + TRYFINALLY = ASSERT = EXEC = EXPR = handleChildren CONTINUE = BREAK = PASS = ignore @@ -512,7 +629,7 @@ Keep track of globals declarations. """ if isinstance(self.scope, FunctionScope): - self.scope.globals.update(dict.fromkeys(node.names)) + self.scope.globals.update(node.names) NONLOCAL = GLOBAL @@ -522,13 +639,23 @@ self.handleNode(gen, node) self.handleNode(node.elt, node) - GENERATOREXP = SETCOMP = LISTCOMP + def GENERATOREXP(self, node): + self.pushScope(GeneratorScope) + # handle generators before element + for gen in node.generators: + self.handleNode(gen, node) + self.handleNode(node.elt, node) + self.popScope() + + SETCOMP = GENERATOREXP def DICTCOMP(self, node): + self.pushScope(GeneratorScope) for gen in node.generators: self.handleNode(gen, node) self.handleNode(node.key, node) self.handleNode(node.value, node) + self.popScope() def FOR(self, node): """ @@ -551,7 +678,7 @@ # 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) + node, varn, self.scope[varn].source) self.handleChildren(node) @@ -559,12 +686,13 @@ """ Handle occurrence of Name (which can be a load/store/delete access.) """ - if node.id == 'locals' and isinstance(node.parent, ast.Call): - # we are doing locals() call in current scope - self.scope.usesLocals = True # Locate the name in locals / function / globals scopes. if isinstance(node.ctx, (ast.Load, ast.AugLoad)): self.handleNodeLoad(node) + if (node.id == 'locals' and isinstance(self.scope, FunctionScope) + and isinstance(node.parent, ast.Call)): + # we are doing locals() call in current scope + self.scope.usesLocals = True elif isinstance(node.ctx, (ast.Store, ast.AugStore)): self.handleNodeStore(node) elif isinstance(node.ctx, ast.Del): @@ -575,12 +703,12 @@ raise RuntimeError("Got impossible expression context: %r" % (node.ctx,)) def FUNCTIONDEF(self, node): - if not hasattr(node, 'decorator_list'): # Python 2.5 - node.decorator_list = node.decorators for deco in node.decorator_list: self.handleNode(deco, node) self.addBinding(node, FunctionDefinition(node.name, node)) self.LAMBDA(node) + if self.withDoctest: + self.deferFunction(lambda: self.handleDoctests(node)) def LAMBDA(self, node): args = [] @@ -593,7 +721,7 @@ else: if arg.id in args: self.report(messages.DuplicateArgument, - node.lineno, arg.id) + node, arg.id) args.append(arg.id) addArgs(node.args.args) defaults = node.args.defaults @@ -601,7 +729,7 @@ for arg in node.args.args + node.args.kwonlyargs: if arg.arg in args: self.report(messages.DuplicateArgument, - node.lineno, arg.arg) + node, arg.arg) args.append(arg.arg) self.handleNode(arg.annotation, node) if hasattr(node, 'returns'): # Only for FunctionDefs @@ -615,14 +743,14 @@ if not wildcard: continue if wildcard in args: - self.report(messages.DuplicateArgument, node.lineno, wildcard) + self.report(messages.DuplicateArgument, node, wildcard) args.append(wildcard) for default in defaults: self.handleNode(default, node) def runFunction(): - self.pushFunctionScope() + self.pushScope() for name in args: self.addBinding(node, Argument(name, node), reportRedef=False) if isinstance(node.body, list): @@ -637,12 +765,8 @@ """ Check to see if any assignments have not been used. """ - for name, binding in self.scope.items(): - if (not binding.used and name not in self.scope.globals - and not self.scope.usesLocals - and isinstance(binding, Assignment)): - self.report(messages.UnusedVariable, - binding.source.lineno, name) + for name, binding in self.scope.unusedAssignments(): + self.report(messages.UnusedVariable, binding.source, name) self.deferAssignment(checkUnusedAssignments) self.popScope() @@ -654,15 +778,16 @@ classes, and the body of its definition. Additionally, add its name to the current scope. """ - # no class decorator in Python 2.5 - for deco in getattr(node, 'decorator_list', ''): + for deco in node.decorator_list: self.handleNode(deco, node) for baseNode in node.bases: self.handleNode(baseNode, node) if not PY2: for keywordNode in node.keywords: self.handleNode(keywordNode, node) - self.pushClassScope() + self.pushScope(ClassScope) + if self.withDoctest: + self.deferFunction(lambda: self.handleDoctests(node)) for stmt in node.body: self.handleNode(stmt, node) self.popScope() @@ -688,21 +813,42 @@ if node.module == '__future__': if not self.futuresAllowed: self.report(messages.LateFutureImport, - node.lineno, [n.name for n in node.names]) + node, [n.name for n in node.names]) else: self.futuresAllowed = False for alias in node.names: if alias.name == '*': self.scope.importStarred = True - self.report(messages.ImportStarUsed, node.lineno, node.module) + self.report(messages.ImportStarUsed, node, node.module) continue name = alias.asname or alias.name importation = Importation(name, node) if node.module == '__future__': - importation.used = (self.scope, node.lineno) + importation.used = (self.scope, node) self.addBinding(node, importation) + def TRY(self, node): + handler_names = [] + # List the exception handlers + for handler in node.handlers: + if isinstance(handler.type, ast.Tuple): + for exc_type in handler.type.elts: + handler_names.append(getNodeName(exc_type)) + elif handler.type: + handler_names.append(getNodeName(handler.type)) + # Memorize the except handlers and process the body + self.exceptHandlers.append(handler_names) + for child in node.body: + self.handleNode(child, node) + self.exceptHandlers.pop() + # Process the other nodes: "except:", "else:", "finally:" + for child in iter_child_nodes(node): + if child not in node.body: + self.handleNode(child, node) + + TRYEXCEPT = TRY + def EXCEPTHANDLER(self, node): # 3.x: in addition to handling children, we must handle the name of # the exception, which is not a Name node, but a simple string.