Return the function node that the input call node targets. 
    
    Note that cases 2b and 2c might make false matches. If two classes are
    imported by the same program and they both have a method with an identical
    name, then class.method() will be associated with the first class in 
    imp_classes.

def getTargetFnDef(node,path,fdefs,cdefs,imp_funcs,imp_mods,imp_classes):
    ''' Return the function node that the input call node targets. 
    
    Note that cases 2b and 2c might make false matches. If two classes are
    imported by the same program and they both have a method with an identical
    name, then class.method() will be associated with the first class in 
    imp_classes.'''
    
    #CASE 1: calling function inside namespace, like foo(x) or randint(x,y)
    if isinstance(node.func,ast.Name):
        # CASE 1A: calling an infile function
        if path in fdefs:
            for x in fdefs[path]:
                if node.func.id == x.name:
                    return x
        # CASE 1B: calling an imported function
        if path in imp_funcs:
            for x in imp_funcs[path]:
                if node.func.id == x.name: 
                    return x
        # CASE 1C: infile object instantiation, i.e. a=Car()
        if path in cdefs:
            for x in cdefs[path]:
                if node.func.id == x.name:
                    classfuncs = [y for y in fdefs[path] if y.pclass==x]
                    initfuncs = [z for z in classfuncs if z.name=='__init__']
                    if initfuncs:
                        return initfuncs[0]
                        
        # CASE 1D: imported object instantiation, from exe import car; a=Car()
        if path in imp_classes:
            for x in imp_classes[path]:
                if node.func.id == x.name:
                    classfuncs = [y for y in fdefs[x.path] if y.pclass==x]
                    initfuncs = [z for z in classfuncs if z.name=='__init__']
                    if initfuncs:
                        return initfuncs[0]
        return None # builtin functions
        
    # CASE 2: calling function outside namespace, like random.randint(x,y)
    elif isinstance(node.func,ast.Attribute):
        try:
            obj    = node.func.value.id
            method = node.func.attr
        except AttributeError:
            return None #weird string thingies
        if obj == 'self':
            return None #setting attrs in class def.
        # CASE 2A: calling imported module.function
        for modpath in imp_mods[path]:
            if not modpath:
                continue
            elif obj+'.py' in modpath:
                if modpath in fdefs:
                    matches = [x for x in fdefs[modpath] if x.name==method]
                    if matches:
                        if len(matches)>1:
                            pass
                            #print("multiple matches found for "+method)
                        return matches[0]
                    
            # CASE 2B: object instantiation with an imported module
            if modpath not in cdefs:
                continue
            for clss in cdefs[modpath]:
                if clss.name==method:
                    classfuncs = [y for y in fdefs[clss.path] if y.pclass==clss]
                    initfuncs = [z for z in classfuncs if z.name=='__init__']
                    if initfuncs:
                        return initfuncs[0]
                    
        # CASE 2C: calling infile class.method 
        if path in cdefs:
            for clss in cdefs[path]:
                for x in fdefs[clss.path]:
                    if x.pclass==clss:
                        return x
                    
        # CASE 2D: calling imported class.method         
        if path in imp_classes:
            for clss in imp_classes[path]:
                for x in fdefs[clss.path]:
                    if x.pclass==clss:
                        if x.name==method:
                            return x
                            
        return None