"""frosted/checker.py.

The core functionality of frosted lives here. Implements the core checking capability models Bindings and Scopes

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

"""
from __future__ import absolute_import, division, print_function, unicode_literals

import builtins
import doctest
import itertools
import os
import sys

from pies import ast
from pies.overrides import *

from frosted import messages

PY34_GTE = sys.version_info >= (3, 4)
FROSTED_BUILTINS = set(dir(builtins) + ['__file__', '__builtins__', '__debug__', '__name__', 'WindowsError',
                                        '__import__'] +
                       os.environ.get('PYFLAKES_BUILTINS', '').split(','))

def node_name(node):
    """
        Convenience function: Returns node.id, or node.name, or None
    """
    return hasattr(node, 'id') and node.id or hasattr(node, 'name') and node.name


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 Assignment for a
    special type of binding that is checked with stricter rules.

    """
    __slots__ = ('name', 'source', 'used')

    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 Importation(Binding):
    """A binding created by an import statement."""
    __slots__ = ('fullName', )

    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."""
    __slots__ = ()


class Definition(Binding):
    """A binding that defines a function or a class."""
    __slots__ = ()


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.

    """
    __slots__ = ()


class FunctionDefinition(Definition):
    __slots__ = ('signature', )

    def __init__(self, name, source):
        super(FunctionDefinition, self).__init__(name, source)
        self.signature = FunctionSignature(source)


class ClassDefinition(Definition):
    __slots__ = ()


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.

    """
    __slots__ = ()

    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.elts:
                if isinstance(node, ast.Str):
                    names.append(node.s)
        return names


class Scope(dict):
    importStarred = False       # set to True when import * is found

    def __repr__(self):
        scope_cls = self.__class__.__name__
        return '<%s at 0x%x %s>' % (scope_cls, id(self), dict.__repr__(self))


class ClassScope(Scope):
    pass


class FunctionScope(Scope):
    """Represents the name scope for a function."""
    uses_locals = False
    always_used = set(['__tracebackhide__', '__traceback_info__', '__traceback_supplement__'])

    def __init__(self):
        Scope.__init__(self)
        self.globals = self.always_used.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.uses_locals
                    and isinstance(binding, Assignment)):
                yield name, binding


class GeneratorScope(Scope):
    pass


class ModuleScope(Scope):
    pass


class FunctionSignature(object):
    __slots__ = ('decorated', 'argument_names', 'default_count', 'kw_only_argument_names', 'default_count',
                 'kw_only_argument_names', 'kw_only_default_count', 'has_var_arg', 'has_kw_arg')

    def __init__(self, node):
        self.decorated = bool(any(node.decorator_list))
        self.argument_names = ast.argument_names(node)
        self.default_count = len(node.args.defaults)
        self.kw_only_argument_names = ast.kw_only_argument_names(node)
        self.kw_only_default_count = ast.kw_only_default_count(node)
        self.has_var_arg = node.args.vararg is not None
        self.has_kw_arg = node.args.kwarg is not None

    def min_argument_count(self):
        return len(self.argument_names) - self.default_count

    def maxArgumentCount(self):
        return len(self.argument_names)

    def checkCall(self, call_node, reporter, name):
        if self.decorated:
            return

        filledSlots = set()
        filledKwOnlySlots = set()
        for item, arg in enumerate(call_node.args):
            if item >= len(self.argument_names):
                if not self.has_var_arg:
                    return reporter.report(messages.TooManyArguments, call_node, name, self.maxArgumentCount())
                break
            filledSlots.add(item)

        for kw in call_node.keywords:
            slots = None
            try:
                argIndex = self.argument_names.index(kw.arg)
                slots = filledSlots
            except ValueError:
                try:
                    argIndex = self.kw_only_argument_names.index(kw.arg)
                    slots = filledKwOnlySlots
                except ValueError:
                    if self.has_kw_arg:
                        continue
                    else:
                        return reporter.report(messages.UnexpectedArgument, call_node, name, kw.arg)
            if argIndex in slots:
                return reporter.report(messages.MultipleValuesForArgument, call_node, name, kw.arg)
            slots.add(argIndex)

        filledSlots.update(range(len(self.argument_names) - self.default_count, len(self.argument_names)))
        filledKwOnlySlots.update(range(len(self.kw_only_argument_names) - self.kw_only_default_count,
                                       len(self.kw_only_argument_names)))

        if (len(filledSlots) < len(self.argument_names) and not call_node.starargs and not call_node.kwargs):
            return reporter.report(messages.TooFewArguments, call_node, name, self.min_argument_count())
        if (len(filledKwOnlySlots) < len(self.kw_only_argument_names) and not call_node.kwargs):
            missing_arguments = [repr(arg) for i, arg in enumerate(self.kw_only_argument_names)
                                if i not in filledKwOnlySlots]
            return reporter.report(messages.NeedKwOnlyArgument, call_node, name, ', '.join(missing_arguments))


class Checker(object):
    """The core of frosted, checks the cleanliness and sanity of Python code."""

    node_depth = 0
    offset = None
    trace_tree = False
    frosted_builtins = FROSTED_BUILTINS

    def __init__(self, tree, filename='(none)', builtins=None, ignore_lines=(), **settings):
        self.settings = settings
        self.ignore_errors = settings.get('ignore_frosted_errors', [])
        self.ignore_lines = ignore_lines
        file_specifc_ignores = settings.get('ignore_frosted_errors_for_' + (os.path.basename(filename) or ""), None)
        if file_specifc_ignores:
            self.ignore_errors += file_specifc_ignores

        self._node_handlers = {}
        self._deferred_functions = []
        self._deferred_assignments = []
        self.dead_scopes = []
        self.messages = []
        self.filename = filename
        if builtins:
            self.frosted_builtins = self.frosted_builtins.union(builtins)
        self.scope_stack = [ModuleScope()]
        self.except_handlers = [()]
        self.futures_allowed = True
        self.root = tree
        self.handle_children(tree)
        self.run_deferred(self._deferred_functions)
        self._deferred_functions = None
        self.run_deferred(self._deferred_assignments)
        self._deferred_assignments = None
        del self.scope_stack[1:]
        self.pop_scope()
        self.check_dead_scopes()

    def defer_function(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._deferred_functions.append((callable, self.scope_stack[:], self.offset))

    def defer_assignment(self, callable):
        """Schedule an assignment handler to be called just after deferred
        function handlers."""
        self._deferred_assignments.append((callable, self.scope_stack[:], self.offset))

    def run_deferred(self, deferred):
        """Run the callables in deferred using their associated scope stack."""
        for handler, scope, offset in deferred:
            self.scope_stack = scope
            self.offset = offset
            handler()

    @property
    def scope(self):
        return self.scope_stack[-1]

    def pop_scope(self):
        self.dead_scopes.append(self.scope_stack.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()
                # Look for possible mistakes in the export list
                if not scope.importStarred and os.path.basename(self.filename) != '__init__.py':
                    undefined = set(all) - set(scope)
                    for name in undefined:
                        self.report(messages.UndefinedExport, scope['__all__'].source, name)
            else:
                all = []

            # Look for imported names that aren't used without checking imports in namespace definition
            for importation in scope.values():
                if isinstance(importation, Importation) and not importation.used and importation.name not in all:
                    self.report(messages.UnusedImport, importation.source, importation.name)

    def push_scope(self, scope_class=FunctionScope):
        self.scope_stack.append(scope_class())

    def push_function_scope(self):    # XXX Deprecated
        self.push_scope(FunctionScope)

    def push_class_scope(self):       # XXX Deprecated
        self.push_scope(ClassScope)

    def report(self, message_class, *args, **kwargs):
        error_code = message_class.error_code

        if(not error_code[:2] + "00" in self.ignore_errors and not error_code in self.ignore_errors and not
           str(message_class.error_number) in self.ignore_errors):
            kwargs['verbose'] = self.settings.get('verbose')
            message = message_class(self.filename, *args, **kwargs)
            if message.lineno not in self.ignore_lines:
                self.messages.append(message)

    def has_parent(self, node, kind):
        while hasattr(node, 'parent'):
            node = node.parent
            if isinstance(node, kind):
                return True

    def get_common_ancestor(self, lnode, rnode, stop=None):
        stop = stop or self.root
        if lnode is rnode:
            return lnode
        if stop in (lnode, rnode):
            return stop

        if not hasattr(lnode, 'parent') or not hasattr(rnode, 'parent'):
            return
        if (lnode.level > rnode.level):
            return self.get_common_ancestor(lnode.parent, rnode, stop)
        if (rnode.level > lnode.level):
            return self.get_common_ancestor(lnode, rnode.parent, stop)
        return self.get_common_ancestor(lnode.parent, rnode.parent, stop)

    def descendant_of(self, node, ancestors, stop=None):
        for ancestor in ancestors:
            if self.get_common_ancestor(node, ancestor, stop) not in (stop, None):
                return True
        return False

    def on_fork(self, parent, lnode, rnode, items):
        return (self.descendant_of(lnode, items, parent) ^ self.descendant_of(rnode, items, parent))

    def different_forks(self, lnode, rnode):
        """True, if lnode and rnode are located on different forks of
        IF/TRY."""
        ancestor = self.get_common_ancestor(lnode, rnode)
        if isinstance(ancestor, ast.If):
            for fork in (ancestor.body, ancestor.orelse):
                if self.on_fork(ancestor, lnode, rnode, fork):
                    return True
        elif isinstance(ancestor, ast.Try):
            body = ancestor.body + ancestor.orelse
            for fork in [body] + [[hdl] for hdl in ancestor.handlers]:
                if self.on_fork(ancestor, lnode, rnode, fork):
                    return True
        elif isinstance(ancestor, ast.TryFinally):
            if self.on_fork(ancestor, lnode, rnode, ancestor.body):
                return True
        return False

    def add_binding(self, node, value, report_redef=True):
        """Called when a binding is altered.

        - `node` is the statement responsible for the change
        - `value` is the optional new value, a Binding instance, associated
        with the binding; if None, the binding is deleted if it exists.
        - if `report_redef` is True (default), rebinding while unused will be
        reported.

        """
        redefinedWhileUnused = False
        if not isinstance(self.scope, ClassScope):
            for scope in self.scope_stack[::-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 report_redef
                        and not self.different_forks(node, existing.source)):
                    redefinedWhileUnused = True
                    self.report(messages.RedefinedWhileUnused,
                                node, value.name, existing.source)

        existing = self.scope.get(value.name)
        if not redefinedWhileUnused and self.has_parent(value.source, ast.ListComp):
            if (existing and report_redef
                    and not self.has_parent(existing.source, (ast.For, ast.ListComp))
                    and not self.different_forks(node, existing.source)):
                self.report(messages.RedefinedInListComp,
                            node, value.name, existing.source)

        if (isinstance(existing, Definition)
                and not existing.used
                and not self.different_forks(node, existing.source)):
            self.report(messages.RedefinedWhileUnused,
                        node, value.name, existing.source)
        else:
            self.scope[value.name] = value

    def get_node_handler(self, node_class):
        try:
            return self._node_handlers[node_class]
        except KeyError:
            nodeType = str(node_class.__name__).upper()
        self._node_handlers[node_class] = handler = getattr(self, nodeType)
        return handler

    def iter_visible_scopes(self):
        outerScopes = itertools.islice(self.scope_stack, len(self.scope_stack) - 1)
        scopes = [scope for scope in outerScopes
                  if isinstance(scope, (FunctionScope, ModuleScope))]
        if (isinstance(self.scope, GeneratorScope)
            and scopes[-1] != self.scope_stack[-2]):
            scopes.append(self.scope_stack[-2])
        scopes.append(self.scope_stack[-1])
        return iter(reversed(scopes))

    def handle_node_load(self, node):
        name = node_name(node)
        if not name:
            return

        importStarred = False
        for scope in self.iter_visible_scopes():
            importStarred = importStarred or scope.importStarred
            try:
                scope[name].used = (self.scope, node)
            except KeyError:
                pass
            else:
                return

        # look in the built-ins
        if importStarred or name in self.frosted_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.except_handlers[-1]:
            self.report(messages.UndefinedName, node, name)

    def handle_node_store(self, node):
        name = node_name(node)
        if not name:
            return
        # if the name hasn't already been defined in the current scope
        if isinstance(self.scope, FunctionScope) and name not in self.scope:
            # for each function or module scope above us
            for scope in self.scope_stack[:-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
                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)
                    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)):
            binding = ExportBinding(name, parent.value)
        else:
            binding = Assignment(name, node)
        if name in self.scope:
            binding.used = self.scope[name].used
        self.add_binding(node, binding)

    def handle_node_delete(self, node):
        name = node_name(node)
        if not name:
            return
        if isinstance(self.scope, FunctionScope) and name in self.scope.globals:
            self.scope.globals.remove(name)
        else:
            try:
                del self.scope[name]
            except KeyError:
                self.report(messages.UndefinedName, node, name)

    def handle_children(self, tree):
        for node in ast.iter_child_nodes(tree):
            self.handleNode(node, tree)

    def is_docstring(self, node):
        """Determine if the given node is a docstring, as long as it is at the
        correct place in the node tree."""
        return isinstance(node, ast.Str) or (isinstance(node, ast.Expr) and
                                             isinstance(node.value, ast.Str))

    def docstring(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
        if self.offset and getattr(node, 'lineno', None) is not None:
            node.lineno += self.offset[0]
            node.col_offset += self.offset[1]
        if self.trace_tree:
            print('  ' * self.node_depth + node.__class__.__name__)
        if self.futures_allowed and not (isinstance(node, ast.ImportFrom) or
                                        self.is_docstring(node)):
            self.futures_allowed = False
        self.node_depth += 1
        node.level = self.node_depth
        node.parent = parent
        try:
            handler = self.get_node_handler(node.__class__)
            handler(node)
        finally:
            self.node_depth -= 1
        if self.trace_tree:
            print('  ' * self.node_depth + 'end ' + node.__class__.__name__)

    _get_doctest_examples = doctest.DocTestParser().get_examples

    def handle_doctests(self, node):
        try:
            docstring, node_lineno = self.docstring(node.body[0])
            if not docstring:
                return
            examples = self._get_doctest_examples(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.push_scope()
        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 or 0))
                self.report(messages.DoctestSyntaxError, node, position)
            else:
                self.offset = (node_offset[0] + node_lineno + example.lineno,
                               node_offset[1] + example.indent + 4)
                self.handle_children(tree)
                self.offset = node_offset
        self.pop_scope()

    def find_return_with_argument(self, node):
        """Finds and returns a return statment that has an argument.

        Note that we should use node.returns in Python 3, but this method is never called in Python 3 so we don't bother
        checking.

        """
        for item in node.body:
            if isinstance(item, ast.Return) and item.value:
                return item
            elif not isinstance(item, ast.FunctionDef) and hasattr(item, 'body'):
                return_with_argument = self.find_return_with_argument(item)
                if return_with_argument:
                    return return_with_argument

    def is_generator(self, node):
        """Checks whether a function is a generator by looking for a yield
        statement or expression."""
        if not isinstance(node.body, list):
            # lambdas can not be generators
            return False
        for item in node.body:
            if isinstance(item, (ast.Assign, ast.Expr)):
                if isinstance(item.value, ast.Yield):
                    return True
            elif not isinstance(item, ast.FunctionDef) and hasattr(item, 'body'):
                if self.is_generator(item):
                    return True
        return False

    def ignore(self, node):
        pass

    # "stmt" type nodes
    RETURN = DELETE = PRINT = WHILE = IF = WITH = WITHITEM = RAISE = TRYFINALLY = ASSERT = EXEC = EXPR = handle_children

    CONTINUE = BREAK = PASS = ignore

    # "expr" type nodes
    BOOLOP = BINOP = UNARYOP = IFEXP = DICT = SET = YIELD = YIELDFROM = COMPARE = REPR = ATTRIBUTE = SUBSCRIPT = \
             LIST = TUPLE = STARRED = NAMECONSTANT = handle_children

    NUM = STR = BYTES = ELLIPSIS = ignore

    # "slice" type nodes
    SLICE = EXTSLICE = INDEX = handle_children

    # expression contexts are node instances too, though being constants
    LOAD = STORE = DEL = AUGLOAD = AUGSTORE = PARAM = ignore

    # same for operators
    AND = OR = ADD = SUB = MULT = DIV = MOD = POW = LSHIFT = RSHIFT = BITOR = BITXOR = BITAND = FLOORDIV = INVERT = \
          NOT = UADD = USUB = EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = ignore

    # additional node types
    COMPREHENSION = KEYWORD = handle_children

    def GLOBAL(self, node):
        """Keep track of globals declarations."""
        if isinstance(self.scope, FunctionScope):
            self.scope.globals.update(node.names)

    NONLOCAL = GLOBAL

    def LISTCOMP(self, node):
        # handle generators before element
        for gen in node.generators:
            self.handleNode(gen, node)
        self.handleNode(node.elt, node)

    def GENERATOREXP(self, node):
        self.push_scope(GeneratorScope)
        # handle generators before element
        for gen in node.generators:
            self.handleNode(gen, node)
        self.handleNode(node.elt, node)
        self.pop_scope()

    SETCOMP = GENERATOREXP

    def DICTCOMP(self, node):
        self.push_scope(GeneratorScope)
        for gen in node.generators:
            self.handleNode(gen, node)
        self.handleNode(node.key, node)
        self.handleNode(node.value, node)
        self.pop_scope()

    def FOR(self, node):
        """Process bindings for loop variables."""
        vars = []

        def collectLoopVars(n):
            if isinstance(n, ast.Name):
                vars.append(n.id)
            elif isinstance(n, ast.expr_context):
                return
            else:
                for c in ast.iter_child_nodes(n):
                    collectLoopVars(c)

        collectLoopVars(node.target)
        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, varn, self.scope[varn].source)

        self.handle_children(node)

    def NAME(self, node):
        """Handle occurrence of Name (which can be a load/store/delete
        access.)"""
        # Locate the name in locals / function / globals scopes.
        if isinstance(node.ctx, (ast.Load, ast.AugLoad)):
            self.handle_node_load(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.uses_locals = True
        elif isinstance(node.ctx, (ast.Store, ast.AugStore)):
            self.handle_node_store(node)
        elif isinstance(node.ctx, ast.Del):
            self.handle_node_delete(node)
        else:
            # must be a Param context -- this only happens for names in function
            # arguments, but these aren't dispatched through here
            raise RuntimeError("Got impossible expression context: %r" % (node.ctx,))

    def CALL(self, node):
        f = node.func
        if isinstance(f, ast.Name):
            for scope in self.iter_visible_scopes():
                definition = scope.get(f.id)
                if definition:
                    if isinstance(definition, FunctionDefinition):
                        definition.signature.checkCall(node, self, f.id)
                    break


        self.handle_children(node)

    def FUNCTIONDEF(self, node):
        for deco in node.decorator_list:
            self.handleNode(deco, node)
        self.add_binding(node, FunctionDefinition(node.name, node))
        self.LAMBDA(node)
        if self.settings.get('run_doctests', False):
            self.defer_function(lambda: self.handle_doctests(node))

    def LAMBDA(self, node):
        args = []
        annotations = []

        if PY2:
            def addArgs(arglist):
                for arg in arglist:
                    if isinstance(arg, ast.Tuple):
                        addArgs(arg.elts)
                    else:
                        if arg.id in args:
                            self.report(messages.DuplicateArgument,
                                        node, arg.id)
                        args.append(arg.id)
            addArgs(node.args.args)
            defaults = node.args.defaults
        else:
            for arg in node.args.args + node.args.kwonlyargs:
                annotations.append(arg.annotation)
                args.append(arg.arg)
            defaults = node.args.defaults + node.args.kw_defaults

        # Only for Python3 FunctionDefs
        is_py3_func = hasattr(node, 'returns')

        for arg_name in ('vararg', 'kwarg'):
            wildcard = getattr(node.args, arg_name)
            if not wildcard:
                continue
            args.append(getattr(wildcard, 'arg', wildcard))
            if is_py3_func:
                if PY34_GTE:
                    annotations.append(wildcard.annotation)
                else:
                    argannotation = arg_name + 'annotation'
                    annotations.append(getattr(node.args, argannotation))
        if is_py3_func:
            annotations.append(node.returns)

        if PY3:
            if len(set(args)) < len(args):
                for (idx, arg) in enumerate(args):
                    if arg in args[:idx]:
                        self.report(messages.DuplicateArgument, node, arg)

        for child in annotations + defaults:
            if child:
                self.handleNode(child, node)

        def runFunction():

            self.push_scope()
            for name in args:
                self.add_binding(node, Argument(name, node), report_redef=False)
            if isinstance(node.body, list):
                # case for FunctionDefs
                for stmt in node.body:
                    self.handleNode(stmt, node)
            else:
                # case for Lambdas
                self.handleNode(node.body, node)

            def checkUnusedAssignments():
                """Check to see if any assignments have not been used."""
                for name, binding in self.scope.unusedAssignments():
                    self.report(messages.UnusedVariable, binding.source, name)
            self.defer_assignment(checkUnusedAssignments)

            if PY2:
                def checkReturnWithArgumentInsideGenerator():
                    """Check to see if there are any return statements with
                    arguments but the function is a generator."""
                    if self.is_generator(node):
                        stmt = self.find_return_with_argument(node)
                        if stmt is not None:
                            self.report(messages.ReturnWithArgsInsideGenerator, stmt)
                self.defer_assignment(checkReturnWithArgumentInsideGenerator)
            self.pop_scope()

        self.defer_function(runFunction)

    def CLASSDEF(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.

        """
        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.push_scope(ClassScope)
        if self.settings.get('run_doctests', False):
            self.defer_function(lambda: self.handle_doctests(node))
        for stmt in node.body:
            self.handleNode(stmt, node)
        self.pop_scope()
        self.add_binding(node, ClassDefinition(node.name, node))

    def ASSIGN(self, node):
        self.handleNode(node.value, node)
        for target in node.targets:
            self.handleNode(target, node)

    def AUGASSIGN(self, node):
        self.handle_node_load(node.target)
        self.handleNode(node.value, node)
        self.handleNode(node.target, node)

    def IMPORT(self, node):
        for alias in node.names:
            name = alias.asname or alias.name
            importation = Importation(name, node)
            self.add_binding(node, importation)

    def IMPORTFROM(self, node):
        if node.module == '__future__':
            if not self.futures_allowed:
                self.report(messages.LateFutureImport,
                            node, [n.name for n in node.names])
        else:
            self.futures_allowed = False

        for alias in node.names:
            if alias.name == '*':
                self.scope.importStarred = True
                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)
            self.add_binding(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(node_name(exc_type))
            elif handler.type:
                handler_names.append(node_name(handler.type))
        # Memorize the except handlers and process the body
        self.except_handlers.append(handler_names)
        for child in node.body:
            self.handleNode(child, node)
        self.except_handlers.pop()
        # Process the other nodes: "except:", "else:", "finally:"
        for child in ast.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.
        if node.type is None:
            self.report(messages.BareExcept, node)
        if isinstance(node.name, str):
            self.handle_node_store(node)
        self.handle_children(node)
