# -*- coding: utf-8 -*-

# Copyright (c) 2021 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a node visitor checking for code that could be simplified.
"""

import ast
import collections
import copy
import itertools

try:
    from ast import unparse
except ImportError:
    # Python < 3.9
    from .ast_unparse import unparse

######################################################################
## The following code is derived from the flake8-simplify package.
##
## Original License:
##
## MIT License
##
## Copyright (c) 2020 Martin Thoma
##
## 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.
######################################################################

BOOL_CONST_TYPES = (ast.Constant, ast.NameConstant)
AST_CONST_TYPES = (ast.Constant, ast.NameConstant, ast.Str, ast.Num)
STR_TYPES = (ast.Constant, ast.Str)


class SimplifyNodeVisitor(ast.NodeVisitor):
    """
    Class to traverse the AST node tree and check for code that can be
    simplified.
    """
    def __init__(self, errorCallback):
        """
        Constructor
        
        @param errorCallback callback function to register an error
        @type func
        """
        super().__init__()
        
        self.__error = errorCallback
        
        self.__classDefinitionStack = []
    
    def visit_Expr(self, node):
        """
        Public method to process an Expr node.
        
        @param node reference to the Expr node
        @type ast.Expr
        """
        self.__check112(node)
        
        self.generic_visit(node)
    
    def visit_Assign(self, node):
        """
        Public method to process an Assign node.
        
        @param node reference to the Assign node
        @type ast.Assign
        """
        self.__check181(node)
        
        self.generic_visit(node)
    
    def visit_BoolOp(self, node):
        """
        Public method to process a BoolOp node.
        
        @param node reference to the BoolOp node
        @type ast.BoolOp
        """
        self.__check101(node)
        self.__check109(node)
        self.__check221(node)
        self.__check222(node)
        self.__check223(node)
        self.__check224(node)
        
        self.generic_visit(node)
    
    def visit_If(self, node):
        """
        Public method to process an If node.
        
        @param node reference to the If node
        @type ast.If
        """
        self.__check102(node)
        self.__check103(node)
        self.__check106(node)
        self.__check108(node)
        self.__check114(node)
        self.__check116(node)
        self.__check122(node)
        
        self.generic_visit(node)
    
    def visit_IfExp(self, node):
        """
        Public method to process an IfExp node.
        
        @param node reference to the IfExp node
        @type ast.IfExp
        """
        self.__check211(node)
        self.__check212(node)
        self.__check213(node)
        
        self.generic_visit(node)
    
    def visit_For(self, node):
        """
        Public method to process a For node.
        
        @param node reference to the For node
        @type ast.For
        """
        self.__check104(node)
        self.__check110_111(node)
        self.__check113(node)
        self.__check118(node)
        
        self.generic_visit(node)
    
    def visit_Try(self, node):
        """
        Public method to process a Try node.
        
        @param node reference to the Try node
        @type ast.Try
        """
        self.__check105(node)
        self.__check107(node)
        
        self.generic_visit(node)
    
    def visit_Call(self, node):
        """
        Public method to process a Call node.
        
        @param node reference to the Call node
        @type ast.Call
        """
        self.__check115(node)
        self.__check182(node)
        self.__check401(node)
        self.__check402(node)
        
        self.generic_visit(node)
    
    def visit_With(self, node):
        """
        Public method to process a With node.
        
        @param node reference to the With node
        @type ast.With
        """
        self.__check117(node)
        
        self.generic_visit(node)
    
    def visit_Compare(self, node):
        """
        Public method to process a Compare node.
        
        @param node reference to the Compare node
        @type ast.Compare
        """
        self.__check118(node)
        self.__check301(node)
        
        self.generic_visit(node)
    
    def visit_ClassDef(self, node):
        """
        Public method to process a ClassDef node.
        
        @param node reference to the ClassDef node
        @type ast.ClassDef
        """
        # register the name of the class being defined
        self.__classDefinitionStack.append(node.name)
        
        self.__check119(node)
        self.__check120_121(node)
        
        self.generic_visit(node)
        
        self.__classDefinitionStack.pop()
    
    def visit_UnaryOp(self, node):
        """
        Public method to process a UnaryOp node.
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        self.__check201(node)
        self.__check202(node)
        self.__check203(node)
        self.__check204(node)
        self.__check205(node)
        self.__check206(node)
        self.__check207(node)
        self.__check208(node)
        
        self.generic_visit(node)
    
    #############################################################
    ## Helper methods for the various checkers below
    #############################################################
    
    def __getDuplicatedIsinstanceCall(self, node):
        """
        Private method to get a list of isinstance arguments which could
        be combined.
        
        @param node reference to the AST node to be inspected
        @type ast.BoolOp
        @return list of variable names of duplicated isinstance calls
        @rtype list of str
        """
        counter = collections.defaultdict(int)
        
        for call in node.values:
            # Ensure this is a call of the built-in isinstance() function.
            if not isinstance(call, ast.Call) or len(call.args) != 2:
                continue
            functionName = unparse(call.func)
            if functionName != "isinstance":
                continue
            
            arg0Name = unparse(call.args[0])
            counter[arg0Name] += 1
        
        return [name for name, count in counter.items() if count > 1]
    
    def __isConstantIncrease(self, expression):
        """
        Private method check the given expression for being a constant
        increase.
        
        @param expression reference to the expression node
        @type ast.AugAssign
        @return flag indicating a constant increase
        @rtype bool
        """
        return (
            isinstance(expression.op, ast.Add) and (
                (isinstance(expression.value, ast.Constant) and
                 isinstance(expression.value.value, int)) or
                isinstance(expression.value, ast.Num)
            )
        )
    
    def __getIfBodyPairs(self, node):
        """
        Private method to extract a list of pairs of test and body for an
        If node.
        
        @param node reference to the If node to be processed
        @type ast.If
        @return list of pairs of test and body
        @rtype list of tuples of (ast.expr, [ast.stmt])
        """
        pairs = [(node.test, node.body)]
        orelse = node.orelse
        while (
            isinstance(orelse, list) and
            len(orelse) == 1 and
            isinstance(orelse[0], ast.If)
        ):
            pairs.append((orelse[0].test, orelse[0].body))
            orelse = orelse[0].orelse
        return pairs
    
    def __isSameBody(self, body1, body2):
        """
        Private method check, if the given bodies are equivalent.
        
        @param body1 list of statements of the first body
        @type list of ast.stmt
        @param body2 list of statements of the second body
        @type list of ast.stmt
        @return flag indicating identical bodies
        @rtype bool
        """
        if len(body1) != len(body2):
            return False
        for a, b in zip(body1, body2):
            try:
                statementEqual = self.__isStatementEqual(a, b)
            except RecursionError:  # maximum recursion depth
                statementEqual = False
            if not statementEqual:
                return False
        
        return True
    
    def __isSameExpression(self, a, b):
        """
        Private method to check, if two expressions are equal.
        
        @param a first expression to be checked
        @type ast.expr
        @param b second expression to be checked
        @type ast.expr
        @return flag indicating equal expressions
        @rtype bool
        """
        if isinstance(a, ast.Name) and isinstance(b, ast.Name):
            return a.id == b.id
        else:
            return False
    
    def __isStatementEqual(self, a, b):
        """
        Private method to check, if two statements are equal.
        
        @param a reference to the first statement
        @type ast.stmt
        @param b reference to the second statement
        @type ast.stmt
        @return flag indicating if the two statements are equal
        @rtype bool
        """
        if type(a) is not type(b):
            return False
        
        if isinstance(a, ast.AST):
            for k, v in vars(a).items():
                if k in ("lineno", "col_offset", "ctx", "end_lineno",
                         "parent"):
                    continue
                if not self.__isStatementEqual(v, getattr(b, k)):
                    return False
            return True
        elif isinstance(a, list):
            return all(itertools.starmap(self.__isStatementEqual, zip(a, b)))
        else:
            return a == b
    
    def __isExceptionCheck(self, node):
        """
        Private method to check, if the node is checking an exception.
        
        @param node reference to the node to be checked
        @type ast.If
        @return flag indicating an exception check
        @rtype bool
        """
        return (
            len(node.body) == 1 and isinstance(node.body[0], ast.Raise)
        )
    
    def __negateTest(self, node):
        """
        Private method negate the given Compare node.
        
        @param node reference to the node to be negated
        @type ast.Compare
        @return node with negated logic
        @rtype ast.Compare
        """
        newNode = copy.deepcopy(node)
        op = newNode.ops[0]
        if isinstance(op, ast.Eq):
            op = ast.NotEq()
        elif isinstance(op, ast.NotEq):
            op = ast.Eq()
        elif isinstance(op, ast.Lt):
            op = ast.GtE()
        elif isinstance(op, ast.LtE):
            op = ast.Gt()
        elif isinstance(op, ast.Gt):
            op = ast.LtE()
        elif isinstance(op, ast.GtE):
            op = ast.Lt()
        elif isinstance(op, ast.Is):
            op = ast.IsNot()
        elif isinstance(op, ast.IsNot):
            op = ast.Is()
        elif isinstance(op, ast.In):
            op = ast.NotIn()
        elif isinstance(op, ast.NotIn):
            op = ast.In()
        newNode.ops = [op]
        return newNode
    
    #############################################################
    ## Methods to check for possible code simplifications below
    #############################################################
    
    def __check101(self, node):
        """
        Private method to check for duplicate isinstance() calls.
        
        @param node reference to the AST node to be checked
        @type ast.BoolOp
        """
        if isinstance(node.op, ast.Or):
            for variable in self.__getDuplicatedIsinstanceCall(node):
                self.__error(node.lineno - 1, node.col_offset, "Y101",
                             variable)
    
    def __check102(self, node):
        """
        Private method to check for nested if statements without else blocks.
        
        @param node reference to the AST node to be checked
        @type ast.If
        """
        # Don't treat 'if __name__ == "__main__":' as an issue.
        if (
            isinstance(node.test, ast.Compare) and
            isinstance(node.test.left, ast.Name) and
            node.test.left.id == "__name__" and
            isinstance(node.test.ops[0], ast.Eq) and
            isinstance(node.test.comparators[0], ast.Constant) and
            node.test.comparators[0].value == "__main__"
        ):
            return
        
        # ## Pattern 1
        # if a: <---
        #     if b: <---
        #         c
        isPattern1 = (
            node.orelse == [] and
            len(node.body) == 1 and
            isinstance(node.body[0], ast.If) and
            node.body[0].orelse == []
        )
        # ## Pattern 2
        # if a: < irrelevant for here
        #     pass
        # elif b:  <--- this is treated like a nested block
        #     if c: <---
        #         d
        if isPattern1:
            self.__error(node.lineno - 1, node.col_offset, "Y102")
    
    def __check103(self, node):
        """
        Private method to check for calls that wrap a condition to return
        a bool.
        
        @param node reference to the AST node to be checked
        @type ast.If
        """
        # if cond:
        #     return True
        # else:
        #     return False
        if not (
            len(node.body) != 1 or
            not isinstance(node.body[0], ast.Return) or
            not isinstance(node.body[0].value, BOOL_CONST_TYPES) or
            not (
                node.body[0].value.value is True or
                node.body[0].value.value is False
            ) or
            len(node.orelse) != 1 or
            not isinstance(node.orelse[0], ast.Return) or
            not isinstance(node.orelse[0].value, BOOL_CONST_TYPES) or
            not (
                node.orelse[0].value.value is True or
                node.orelse[0].value.value is False
            )
        ):
            condition = unparse(node.test)
            self.__error(node.lineno - 1, node.col_offset, "Y103", condition)
    
    def __check104(self, node):
        """
        Private method to check for "iterate and yield" patterns.
        
        @param node reference to the AST node to be checked
        @type ast.For
        """
        # for item in iterable:
        #     yield item
        if not (
            len(node.body) != 1 or
            not isinstance(node.body[0], ast.Expr) or
            not isinstance(node.body[0].value, ast.Yield) or
            not isinstance(node.target, ast.Name) or
            not isinstance(node.body[0].value.value, ast.Name) or
            node.target.id != node.body[0].value.value.id or
            node.orelse != []
        ):
            iterable = unparse(node.iter)
            self.__error(node.lineno - 1, node.col_offset, "Y104", iterable)
    
    def __check105(self, node):
        """
        Private method to check for "try-except-pass" patterns.
        
        @param node reference to the AST node to be checked
        @type ast.Try
        """
        # try:
        #     foo()
        # except ValueError:
        #     pass
        if not (
            len(node.handlers) != 1 or
            not isinstance(node.handlers[0], ast.ExceptHandler) or
            len(node.handlers[0].body) != 1 or
            not isinstance(node.handlers[0].body[0], ast.Pass) or
            node.orelse != []
        ):
            if node.handlers[0].type is None:
                exception = "Exception"
            elif isinstance(node.handlers[0].type, ast.Tuple):
                exception = ", ".join(
                    [unparse(n) for n in node.handlers[0].type.elts])
            else:
                exception = unparse(node.handlers[0].type)
            self.__error(node.lineno - 1, node.col_offset, "Y105", exception)
    
    def __check106(self, node):
        """
        Private method to check for calls where an exception is raised in else.
        
        @param node reference to the AST node to be checked
        @type ast.If
        """
        # if cond:
        #     return True
        # else:
        #     raise Exception
        just_one = (
            len(node.body) == 1 and
            len(node.orelse) >= 1 and
            isinstance(node.orelse[-1], ast.Raise) and
            not isinstance(node.body[-1], ast.Raise)
        )
        many = (
            len(node.body) > 2 * len(node.orelse) and
            len(node.orelse) >= 1 and
            isinstance(node.orelse[-1], ast.Raise) and
            not isinstance(node.body[-1], ast.Raise)
        )
        if just_one or many:
            self.__error(node.lineno - 1, node.col_offset, "Y106")
    
    def __check107(self, node):
        """
        Private method to check for calls where try/except and finally have
        'return'.
        
        @param node reference to the AST node to be checked
        @type ast.Try
        """
        # def foo():
        #     try:
        #         1 / 0
        #         return "1"
        #     except:
        #         return "2"
        #     finally:
        #         return "3"
        tryHasReturn = False
        for stmt in node.body:
            if isinstance(stmt, ast.Return):
                tryHasReturn = True
                break

        exceptHasReturn = False
        for stmt2 in node.handlers:
            if isinstance(stmt2, ast.Return):
                exceptHasReturn = True
                break

        finallyHasReturn = False
        finallyReturn = None
        for stmt in node.finalbody:
            if isinstance(stmt, ast.Return):
                finallyHasReturn = True
                finallyReturn = stmt
                break

        if (
            (tryHasReturn or exceptHasReturn) and
            finallyHasReturn and
            finallyReturn is not None
        ):
            self.__error(finallyReturn.lineno - 1,
                         finallyReturn.col_offset, "Y107")
    
    def __check108(self, node):
        """
        Private method to check for if-elses which could be a ternary
        operator assignment.
        
        @param node reference to the AST node to be checked
        @type ast.If
        """
        # if a:
        #     b = c
        # else:
        #     b = d
        #
        # but not:
        # if a:
        #     b = c
        # elif c:
        #     b = e
        # else:
        #     b = d
        if (
            len(node.body) == 1 and
            len(node.orelse) == 1 and
            isinstance(node.body[0], ast.Assign) and
            isinstance(node.orelse[0], ast.Assign) and
            len(node.body[0].targets) == 1 and
            len(node.orelse[0].targets) == 1 and
            isinstance(node.body[0].targets[0], ast.Name) and
            isinstance(node.orelse[0].targets[0], ast.Name) and
            node.body[0].targets[0].id == node.orelse[0].targets[0].id and
            not isinstance(node.parent, ast.If)
        ):
            assign = unparse(node.body[0].targets[0])
            body = unparse(node.body[0].value)
            cond = unparse(node.test)
            orelse = unparse(node.orelse[0].value)
            
            self.__error(node.lineno - 1, node.col_offset, "Y108",
                         assign, body, cond, orelse)
    
    def __check109(self, node):
        """
        Private method to check for multiple equalities with the same value
        are combined via "or".
        
        @param node reference to the AST node to be checked
        @type ast.BoolOp
        """
        # if a == b or a == c:
        #     d
        if isinstance(node.op, ast.Or):
            equalities = [
                value
                for value in node.values
                if isinstance(value, ast.Compare) and
                len(value.ops) == 1 and
                isinstance(value.ops[0], ast.Eq)
            ]
            ids = []  # (name, compared_to)
            for eq in equalities:
                if isinstance(eq.left, ast.Name):
                    ids.append((eq.left, eq.comparators[0]))
                if (
                    len(eq.comparators) == 1 and
                    isinstance(eq.comparators[0], ast.Name)
                ):
                    ids.append((eq.comparators[0], eq.left))

            id2count = {}
            for identifier, comparedTo in ids:
                if identifier.id not in id2count:
                    id2count[identifier.id] = []
                id2count[identifier.id].append(comparedTo)
            for value, values in id2count.items():
                if len(values) == 1:
                    continue
                
                self.__error(node.lineno - 1, node.col_offset, "Y109",
                             value, unparse(ast.Tuple(elts=values)),
                             unparse(node))
    
    def __check110_111(self, node):
        """
        Private method to check if any / all could be used.
        
        @param node reference to the AST node to be checked
        @type ast.For
        """
        # for x in iterable:
        #     if check(x):
        #         return True
        # return False
        #
        # for x in iterable:
        #     if check(x):
        #         return False
        # return True
        if (
            len(node.body) == 1 and
            isinstance(node.body[0], ast.If) and
            len(node.body[0].body) == 1 and
            isinstance(node.body[0].body[0], ast.Return) and
            isinstance(node.body[0].body[0].value, BOOL_CONST_TYPES) and
            hasattr(node.body[0].body[0].value, "value")
        ):
            check = unparse(node.body[0].test)
            target = unparse(node.target)
            iterable = unparse(node.iter)
            if node.body[0].body[0].value.value is True:
                self.__error(node.lineno - 1, node.col_offset, "Y110",
                             check, target, iterable)
            elif node.body[0].body[0].value.value is False:
                check = "not " + check
                if check.startswith("not not "):
                    check = check[len("not not "):]
                self.__error(node.lineno - 1, node.col_offset, "Y111",
                             check, target, iterable)
    
    def __check112(self, node):
        """
        Private method to check for non-capitalized calls to environment
        variables.
        
        @param node reference to the AST node to be checked
        @type ast.Expr
        """
        # os.environ["foo"]
        # os.environ.get("bar")
        isIndexCall = (
            isinstance(node.value, ast.Subscript) and
            isinstance(node.value.value, ast.Attribute) and
            isinstance(node.value.value.value, ast.Name) and
            node.value.value.value.id == "os" and
            node.value.value.attr == "environ" and
            (
                (
                    isinstance(node.value.slice, ast.Index) and
                    isinstance(node.value.slice.value, STR_TYPES)
                ) or
                isinstance(node.value.slice, ast.Constant)
            )
        )
        if isIndexCall:
            subscript = node.value
            slice_ = subscript.slice
            if isinstance(slice_, ast.Index):
                # Python < 3.9
                stringPart = slice_.value  # type: ignore
                if isinstance(stringPart, ast.Str):
                    envName = stringPart.s  # Python 3.6 / 3.7 fallback
                else:
                    envName = stringPart.value
            elif isinstance(slice_, ast.Constant):
                # Python 3.9
                envName = slice_.value

            # Check if this has a change
            hasChange = envName != envName.upper()

        isGetCall = (
            isinstance(node.value, ast.Call) and
            isinstance(node.value.func, ast.Attribute) and
            isinstance(node.value.func.value, ast.Attribute) and
            isinstance(node.value.func.value.value, ast.Name) and
            node.value.func.value.value.id == "os" and
            node.value.func.value.attr == "environ" and
            node.value.func.attr == "get" and
            len(node.value.args) in [1, 2] and
            isinstance(node.value.args[0], STR_TYPES)
        )
        if isGetCall:
            call = node.value
            stringPart = call.args[0]
            if isinstance(stringPart, ast.Str):
                envName = stringPart.s  # Python 3.6 / 3.7 fallback
            else:
                envName = stringPart.value
            # Check if this has a change
            hasChange = envName != envName.upper()
        if not (isIndexCall or isGetCall) or not hasChange:
            return
        if isIndexCall:
            original = unparse(node)
            expected = f"os.environ['{envName.upper()}']"
        elif isGetCall:
            original = unparse(node)
            if len(node.value.args) == 1:
                expected = f"os.environ.get('{envName.upper()}')"
            else:
                defaultValue = unparse(node.value.args[1])
                expected = (
                    f"os.environ.get('{envName.upper()}', '{defaultValue}')"
                )
        else:
            return
        
        self.__error(node.lineno - 1, node.col_offset, "Y112", expected,
                     original)
    
    def __check113(self, node):
        """
        Private method to check for loops in which "enumerate" should be
        used.
        
        @param node reference to the AST node to be checked
        @type ast.For
        """
        # idx = 0
        # for el in iterable:
        #     ...
        #     idx += 1
        variableCandidates = []
        for expression in node.body:
            if (
                isinstance(expression, ast.AugAssign) and
                self.__isConstantIncrease(expression) and
                isinstance(expression.target, ast.Name)
            ):
                variableCandidates.append(expression.target)

        for candidate in variableCandidates:
            self.__error(candidate.lineno - 1, candidate.col_offset, "Y113",
                         unparse(candidate))
    
    def __check114(self, node):
        """
        Private method to check for alternative if clauses with identical
        bodies.
        
        @param node reference to the AST node to be checked
        @type ast.If
        """
        # if a:
        #     b
        # elif c:
        #     b
        ifBodyPairs = self.__getIfBodyPairs(node)
        errorPairs = []
        for ifbody1, ifbody2 in itertools.combinations(ifBodyPairs, 2):
            if self.__isSameBody(ifbody1[1], ifbody2[1]):
                errorPairs.append((ifbody1, ifbody2))
        for ifbody1, ifbody2 in errorPairs:
            self.__error(ifbody1[0].lineno - 1, ifbody1[0].col_offset, "Y114",
                         unparse(ifbody1[0]), unparse(ifbody2[0]))
    
    def __check115(self, node):
        """
        Private method to to check for places where open() is called without
        a context handler.
        
        @param node reference to the AST node to be checked
        @type ast.Call
        """
        # f = open(...)
        #. ..  # (do something with f)
        # f.close()
        if (
            isinstance(node.func, ast.Name) and
            node.func.id == "open" and
            not isinstance(node.parent, ast.withitem)
        ):
            self.__error(node.lineno - 1, node.col_offset, "Y115")
    
    def __check116(self, node):
        """
        Private method to check for places with 3 or more consecutive
        if-statements with direct returns.
        
        * Each if-statement must be a check for equality with the
          same variable
        * Each if-statement must just have a "return"
        * Else must also just have a return
        
        @param node reference to the AST node to be checked
        @type ast.If
        """
        # if a == "foo":
        #     return "bar"
        # elif a == "bar":
        #     return "baz"
        # elif a == "boo":
        #     return "ooh"
        # else:
        #    return 42
        if (
            isinstance(node.test, ast.Compare) and
            isinstance(node.test.left, ast.Name) and
            len(node.test.ops) == 1 and
            isinstance(node.test.ops[0], ast.Eq) and
            len(node.test.comparators) == 1 and
            isinstance(node.test.comparators[0], AST_CONST_TYPES) and
            len(node.body) == 1 and
            isinstance(node.body[0], ast.Return) and
            len(node.orelse) == 1 and
            isinstance(node.orelse[0], ast.If)
        ):
            variable = node.test.left
            child = node.orelse[0]
            elseValue = None
            if node.body[0].value is not None:
                bodyValueStr = unparse(node.body[0].value).strip("'")
            else:
                bodyValueStr = "None"
            if isinstance(node.test.comparators[0], ast.Str):
                keyValuePairs = {
                    node.test.comparators[0].s: bodyValueStr
                }
            elif isinstance(node.test.comparators[0], ast.Num):
                keyValuePairs = {
                    node.test.comparators[0].n: bodyValueStr,
                }
            else:
                keyValuePairs = {
                    node.test.comparators[0].value: bodyValueStr
                }
            while child:
                if not (
                    isinstance(child.test, ast.Compare) and
                    isinstance(child.test.left, ast.Name) and
                    child.test.left.id == variable.id and
                    len(child.test.ops) == 1 and
                    isinstance(child.test.ops[0], ast.Eq) and
                    len(child.test.comparators) == 1 and
                    isinstance(child.test.comparators[0], AST_CONST_TYPES) and
                    len(child.body) == 1 and
                    isinstance(child.body[0], ast.Return) and
                    len(child.orelse) <= 1
                ):
                    return
                
                if isinstance(child.test.comparators[0], ast.Str):
                    key = child.test.comparators[0].s
                elif isinstance(child.test.comparators[0], ast.Num):
                    key = child.test.comparators[0].n
                else:
                    key = child.test.comparators[0].value
                keyValuePairs[key] = unparse(child.body[0].value).strip("'")
                if len(child.orelse) == 1:
                    if isinstance(child.orelse[0], ast.If):
                        child = child.orelse[0]
                    elif isinstance(child.orelse[0], ast.Return):
                        elseValue = unparse(child.orelse[0].value)
                        child = None
                    else:
                        return
                else:
                    child = None
            
            if len(keyValuePairs) < 3:
                return
            
            if elseValue:
                ret = f"{keyValuePairs}.get({variable.id}, {elseValue})"
            else:
                ret = f"{keyValuePairs}.get({variable.id})"
            
            self.__error(node.lineno - 1, node.col_offset, "Y116", ret)
    
    def __check117(self, node):
        """
        Private method to check for multiple with-statements with same scope.
        
        @param node reference to the AST node to be checked
        @type ast.With
        """
        # with A() as a:
        #     with B() as b:
        #         print("hello")
        if (
            len(node.body) == 1 and
            isinstance(node.body[0], ast.With)
        ):
            withItems = []
            for withitem in node.items + node.body[0].items:
                withItems.append(f"{unparse(withitem)}")
            mergedWith = f"with {', '.join(withItems)}:"
            self.__error(node.lineno - 1, node.col_offset, "Y117", mergedWith)
    
    def __check118(self, node):
        """
        Private method to check for usages of "key in dict.keys()".
        
        @param node reference to the AST node to be checked
        @type ast.Compare or ast.For
        """
        # Pattern 1:
        #
        # if key in dict.keys():
        #     # do something
        #
        # Pattern 2:
        #
        # for key in dict.keys():
        #     # do something
        if (
            isinstance(node, ast.Compare) and
            len(node.ops) == 1 and
            isinstance(node.ops[0], ast.In) and
            len(node.comparators) == 1
        ):
            callNode = node.comparators[0]
        elif (
            isinstance(node, ast.For)
        ):
            callNode = node.iter
        else:
            callNode = None
        
        if not isinstance(callNode, ast.Call):
            return
        
        attrNode = callNode.func
        if (
            isinstance(callNode.func, ast.Attribute) and
            callNode.func.attr == "keys" and
            isinstance(callNode.func.ctx, ast.Load)
        ):
            if isinstance(node, ast.Compare):
                keyStr = unparse(node.left)
            else:
                keyStr = unparse(node.target)
            dictStr = unparse(attrNode.value)
            self.__error(node.lineno - 1, node.col_offset, "Y118",
                         keyStr, dictStr)
    
    def __check119(self, node):
        """
        Private method to check for classes that should be "dataclasses".
        
        @param node reference to the AST node to be checked
        @type ast.ClassDef
        """
        if (
            len(node.decorator_list) == 0 and
            len(node.bases) == 0
        ):
            dataclassFunctions = [
                "__init__",
                "__eq__",
                "__hash__",
                "__repr__",
                "__str__",
            ]
            hasOnlyConstructorMethod = True
            for bodyElement in node.body:
                if (
                    isinstance(bodyElement, ast.FunctionDef) and
                    bodyElement.name not in dataclassFunctions
                ):
                    hasOnlyConstructorMethod = False
                    break

            if (
                hasOnlyConstructorMethod and
                sum(1 for el in node.body
                    if isinstance(el, ast.FunctionDef)) > 0
            ):
                self.__error(node.lineno - 1, node.col_offset, "Y119",
                             node.name)
    
    def __check120_121(self, node):
        """
        Private method to check for classes that inherit from object.
        
        @param node reference to the AST node to be checked
        @type ast.ClassDef
        """
        # class FooBar(object):
        #     ...
        if (
            len(node.bases) == 1 and
            isinstance(node.bases[0], ast.Name) and
            node.bases[0].id == "object"
        ):
            self.__error(node.lineno - 1, node.col_offset, "Y120",
                         node.name)
        
        elif (
            len(node.bases) > 1 and
            isinstance(node.bases[-1], ast.Name) and
            node.bases[-1].id == "object"
        ):
            self.__error(node.lineno - 1, node.col_offset, "Y121",
                         node.name, ", ".join(b.id for b in node.bases[:-1]))
    
    def __check122(self, node):
        """
        Private method to check for all if-blocks which only check if a key
        is in a dictionary.
        
        @param node reference to the AST node to be checked
        @type ast.If
        """
        if (
            isinstance(node.test, ast.Compare) and
            len(node.test.ops) == 1 and
            isinstance(node.test.ops[0], ast.In) and
            len(node.body) == 1 and
            len(node.orelse) == 0
        ) and (
            # We might still be left with a check if a value is in a list or
            # in the body the developer might remove the element from the list.
            # We need to have a look at the body.
            isinstance(node.body[0], ast.Assign) and
            isinstance(node.body[0].value, ast.Subscript) and
            len(node.body[0].targets) == 1 and
            isinstance(node.body[0].targets[0], ast.Name) and
            isinstance(node.body[0].value.value, ast.Name) and
            isinstance(node.test.comparators[0], ast.Name) and
            node.body[0].value.value.id == node.test.comparators[0].id
        ):
            key = unparse(node.test.left)
            dictname = unparse(node.test.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y122",
                         dictname, key)
    
    def __check181(self, node):
        """
        Private method to check for assignments that could be converted into
        an augmented assignment.
        
        @param node reference to the AST node to be checked
        @type ast.Assign
        """
        # a = a - b
        if (
            len(node.targets) == 1 and
            isinstance(node.targets[0], ast.Name) and
            isinstance(node.value, ast.BinOp) and
            isinstance(node.value.left, ast.Name) and
            node.value.left.id == node.targets[0].id and
            not isinstance(node.value.right, ast.Tuple)
        ):
            newNode = ast.AugAssign(node.targets[0], node.value.op,
                                    node.value.right)
            self.__error(node.lineno - 1, node.col_offset, "Y181",
                         unparse(newNode), unparse(node))
    
    def __check182(self, node):
        """
        Private method to check for calls of type 'super()' that could
        be shortened to 'super()'.
        
        @param node reference to the AST node to be checked
        @type ast.Call
        """
        # super()
        if (
            self.__classDefinitionStack and
            isinstance(node.func, ast.Name) and
            node.func.id == "super" and
            len(node.args) == 2 and
            all(isinstance(arg, ast.Name) for arg in node.args) and
            node.args[0].id == self.__classDefinitionStack[-1] and
            node.args[1].id == "self"
        ):
            self.__error(node.lineno - 1, node.col_offset, "Y182",
                         unparse(node))
    
    def __check201(self, node):
        """
        Private method to check for calls where an unary 'not' is used for
        an unequality.
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        # not a == b
        if not (
            (
                not isinstance(node.op, ast.Not) or
                not isinstance(node.operand, ast.Compare) or
                len(node.operand.ops) != 1 or
                not isinstance(node.operand.ops[0], ast.Eq)
            ) or
            isinstance(node.parent, ast.If) and
            self.__isExceptionCheck(node.parent)
        ):
            comparison = node.operand
            left = unparse(comparison.left)
            right = unparse(comparison.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y201",
                         left, right)
    
    def __check202(self, node):
        """
        Private method to check for calls where an unary 'not' is used for
        an equality.
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        # not a != b
        if not (
            (
                not isinstance(node.op, ast.Not) or
                not isinstance(node.operand, ast.Compare) or
                len(node.operand.ops) != 1 or
                not isinstance(node.operand.ops[0], ast.NotEq)
            ) or
            isinstance(node.parent, ast.If) and
            self.__isExceptionCheck(node.parent)
        ):
            comparison = node.operand
            left = unparse(comparison.left)
            right = unparse(comparison.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y202",
                         left, right)
    
    def __check203(self, node):
        """
        Private method to check for calls where an unary 'not' is used for
        an in-check.
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        # not a in b
        if not (
            (
                not isinstance(node.op, ast.Not) or
                not isinstance(node.operand, ast.Compare) or
                len(node.operand.ops) != 1 or
                not isinstance(node.operand.ops[0], ast.In)
            ) or
            isinstance(node.parent, ast.If) and
            self.__isExceptionCheck(node.parent)
        ):
            comparison = node.operand
            left = unparse(comparison.left)
            right = unparse(comparison.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y203",
                         left, right)
    
    def __check204(self, node):
        """
        Private method to check for calls of the type "not (a < b)".
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        # not a < b
        if not (
            (
                not isinstance(node.op, ast.Not) or
                not isinstance(node.operand, ast.Compare) or
                len(node.operand.ops) != 1 or
                not isinstance(node.operand.ops[0], ast.Lt)
            ) or
            isinstance(node.parent, ast.If) and
            self.__isExceptionCheck(node.parent)
        ):
            comparison = node.operand
            left = unparse(comparison.left)
            right = unparse(comparison.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y204",
                         left, right)
    
    def __check205(self, node):
        """
        Private method to check for calls of the type "not (a <= b)".
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        # not a <= b
        if not (
            (
                not isinstance(node.op, ast.Not) or
                not isinstance(node.operand, ast.Compare) or
                len(node.operand.ops) != 1 or
                not isinstance(node.operand.ops[0], ast.LtE)
            ) or
            isinstance(node.parent, ast.If) and
            self.__isExceptionCheck(node.parent)
        ):
            comparison = node.operand
            left = unparse(comparison.left)
            right = unparse(comparison.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y205",
                         left, right)
    
    def __check206(self, node):
        """
        Private method to check for calls of the type "not (a > b)".
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        # not a > b
        if not (
            (
                not isinstance(node.op, ast.Not) or
                not isinstance(node.operand, ast.Compare) or
                len(node.operand.ops) != 1 or
                not isinstance(node.operand.ops[0], ast.Gt)
            ) or
            isinstance(node.parent, ast.If) and
            self.__isExceptionCheck(node.parent)
        ):
            comparison = node.operand
            left = unparse(comparison.left)
            right = unparse(comparison.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y206",
                         left, right)
    
    def __check207(self, node):
        """
        Private method to check for calls of the type "not (a >= b)".
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        # not a >= b
        if not (
            (
                not isinstance(node.op, ast.Not) or
                not isinstance(node.operand, ast.Compare) or
                len(node.operand.ops) != 1 or
                not isinstance(node.operand.ops[0], ast.GtE)
            ) or
            isinstance(node.parent, ast.If) and
            self.__isExceptionCheck(node.parent)
        ):
            comparison = node.operand
            left = unparse(comparison.left)
            right = unparse(comparison.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y207",
                         left, right)
    
    def __check208(self, node):
        """
        Private method to check for calls of the type "not (not a)".
        
        @param node reference to the UnaryOp node
        @type ast.UnaryOp
        """
        # not (not a)
        if (
            isinstance(node.op, ast.Not) and
            isinstance(node.operand, ast.UnaryOp) and
            isinstance(node.operand.op, ast.Not)
        ):
            var = unparse(node.operand.operand)
            self.__error(node.lineno - 1, node.col_offset, "Y208", var)
    
    def __check211(self, node):
        """
        Private method to check for calls of the type "True if a else False".
        
        @param node reference to the AST node to be checked
        @type ast.IfExp
        """
        # True if a else False
        if (
            isinstance(node.body, BOOL_CONST_TYPES) and
            node.body.value is True and
            isinstance(node.orelse, BOOL_CONST_TYPES) and
            node.orelse.value is False
        ):
            cond = unparse(node.test)
            if isinstance(node.test, ast.Name):
                newCond = "bool({0})".format(cond)
            else:
                newCond = cond
            self.__error(node.lineno - 1, node.col_offset, "Y211",
                         cond, newCond)
    
    def __check212(self, node):
        """
        Private method to check for calls of the type "False if a else True".
        
        @param node reference to the AST node to be checked
        @type ast.IfExp
        """
        # False if a else True
        if (
            isinstance(node.body, BOOL_CONST_TYPES) and
            node.body.value is False and
            isinstance(node.orelse, BOOL_CONST_TYPES) and
            node.orelse.value is True
        ):
            cond = unparse(node.test)
            if isinstance(node.test, ast.Name):
                newCond = "not {0}".format(cond)
            else:
                if len(node.test.ops) == 1:
                    newCond = unparse(self.__negateTest(node.test))
                else:
                    newCond = "not ({0})".format(cond)
            self.__error(node.lineno - 1, node.col_offset, "Y212",
                         cond, newCond)
    
    def __check213(self, node):
        """
        Private method to check for calls of the type "b if not a else a".
        
        @param node reference to the AST node to be checked
        @type ast.IfExp
        """
        # b if not a else a
        if (
            isinstance(node.test, ast.UnaryOp) and
            isinstance(node.test.op, ast.Not) and
            self.__isSameExpression(node.test.operand, node.orelse)
        ):
            a = unparse(node.test.operand)
            b = unparse(node.body)
            self.__error(node.lineno - 1, node.col_offset, "Y213", a, b)
    
    def __check221(self, node):
        """
        Private method to check for calls of the type "a and not a".
        
        @param node reference to the AST node to be checked
        @type ast.BoolOp
        """
        # a and not a
        if (
            isinstance(node.op, ast.And) and
            len(node.values) >= 2
        ):
            # We have a boolean And. Let's make sure there is two times the
            # same expression, but once with a "not"
            negatedExpressions = []
            nonNegatedExpressions = []
            for exp in node.values:
                if (
                    isinstance(exp, ast.UnaryOp) and
                    isinstance(exp.op, ast.Not)
                ):
                    negatedExpressions.append(exp.operand)
                else:
                    nonNegatedExpressions.append(exp)
            for negatedExpression in negatedExpressions:
                for nonNegatedExpression in nonNegatedExpressions:
                    if self.__isSameExpression(
                        negatedExpression, nonNegatedExpression
                    ):
                        negExp = unparse(negatedExpression)
                        self.__error(node.lineno - 1, node.col_offset, "Y221",
                                     negExp)
    
    def __check222(self, node):
        """
        Private method to check for calls of the type "a or not a".
        
        @param node reference to the AST node to be checked
        @type ast.BoolOp
        """
        # a or not a
        if (
            isinstance(node.op, ast.Or) and
            len(node.values) >= 2
        ):
            # We have a boolean And. Let's make sure there is two times the
            # same expression, but once with a "not"
            negatedExpressions = []
            nonNegatedExpressions = []
            for exp in node.values:
                if (
                    isinstance(exp, ast.UnaryOp) and
                    isinstance(exp.op, ast.Not)
                ):
                    negatedExpressions.append(exp.operand)
                else:
                    nonNegatedExpressions.append(exp)
            for negatedExpression in negatedExpressions:
                for nonNegatedExpression in nonNegatedExpressions:
                    if self.__isSameExpression(
                        negatedExpression, nonNegatedExpression
                    ):
                        negExp = unparse(negatedExpression)
                        self.__error(node.lineno - 1, node.col_offset, "Y222",
                                     negExp)
    
    def __check223(self, node):
        """
        Private method to check for calls of the type "... or True".
        
        @param node reference to the AST node to be checked
        @type ast.BoolOp
        """
        # a or True
        if isinstance(node.op, ast.Or):
            for exp in node.values:
                if isinstance(exp, BOOL_CONST_TYPES) and exp.value is True:
                    self.__error(node.lineno - 1, node.col_offset, "Y223")
    
    def __check224(self, node):
        """
        Private method to check for calls of the type "... and False".
        
        @param node reference to the AST node to be checked
        @type ast.BoolOp
        """
        # a and False
        if isinstance(node.op, ast.And):
            for exp in node.values:
                if isinstance(exp, BOOL_CONST_TYPES) and exp.value is False:
                    self.__error(node.lineno - 1, node.col_offset, "Y224")
    
    def __check301(self, node):
        """
        Private method to check for Yoda conditions.
        
        @param node reference to the AST node to be checked
        @type ast.Compare
        """
        # 42 == age
        if (
            isinstance(node.left, AST_CONST_TYPES) and
            len(node.ops) == 1 and
            isinstance(node.ops[0], ast.Eq)
        ):
            left = unparse(node.left)
            isPy37Str = isinstance(node.left, ast.Str)
            isPy38Str = (
                isinstance(node.left, ast.Constant) and
                isinstance(node.left.value, str)
            )
            if isPy37Str or isPy38Str:
                left = f"'{left}'"
            right = unparse(node.comparators[0])
            self.__error(node.lineno - 1, node.col_offset, "Y301",
                         left, right)
    
    def __check401(self, node):
        """
        Private method to check for bare boolean function arguments.
        
        @param node reference to the AST node to be checked
        @type ast.Call
        """
        # foo(a, b, True)
        hasBareBool = any(
            isinstance(callArg, ast.Constant) and
            (callArg.value is True or callArg.value is False)
            for callArg in node.args
        )

        isException = (
            isinstance(node.func, ast.Attribute) and
            node.func.attr in ["get"]
        )
        
        if hasBareBool and not isException:
            self.__error(node.lineno - 1, node.col_offset, "Y401")
    
    def __check402(self, node):
        """
        Private method to check for bare numeric function arguments.
        
        @param node reference to the AST node to be checked
        @type ast.Call
        """
        # foo(a, b, 123123)
        hasBareNumeric = any(
            isinstance(callArg, ast.Constant) and
            type(callArg.value) in (float, int)
            for callArg in node.args
        )

        isException = (
            isinstance(node.func, ast.Name) and
            node.func.id == "range"
        )
        isException = isException or (
            isinstance(node.func, ast.Attribute) and
            node.func.attr in ("get", "insert")
        )
        
        if hasBareNumeric and not isException:
            self.__error(node.lineno - 1, node.col_offset, "Y402")

#
# eflag: noqa = M891
