docs: 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