Repository: joernio/joern
Pull request #2907 (2023-06-20 12:22:52)

Unifying Import Resolution
Import resolution until now has been integrated into the type recovery pass. The type recovery pass is an iterate-and-propagate algorithm, where import resolution would be run every iteration for every file. Since the type recovery operates on a fork-join concurrency (since inter-file writing is involved), whereas the import resolution alone is mostly read-only, there is room for optimization. The following changes are made:
* Introduced the `XImportPass` class, which unifies the two `ImportPass` classes from `jssrc2cpg` and `pysrc2cpg`, and implements them using the `ConcurrentWriterCpgPass`.
* Introduced a `XImportResolutionPass` class, which uses the `Tag` node feature to resolve the entities that the `ImportNode`s refer to. The values of the `Tag` node are serialized versions of a `ResolvedImport` trait which is implemented under `XImportResolutionPass` as a number of classes which map the CPG entities being imported (or indicates when they're unknown or are external). `XImportResolutionPass` implements `ConcurrentWriterCpgPass`.
* Removed the import recovery from the `jssrc2cpg` and `pysrc2cpg`'s `XTypeRecovery` implementations and instead added import parsing of these `Tag` nodes in `XTypeRecovery` themselves.

The result is a fast, parallel, import resolution that is run exactly once and a much simpler type recovery that focuses only on the context of the compilation unit for the iterate-and-propagate recovery algorithm.

Breaking changes: `jssrc2cpg.passes.ImportResolutionPass` and `pysrc2cpg.ImportResolutionPass` need to be included after their respective `ImportPass`.


Patch:
diff --git a/console/src/main/scala/io/joern/console/cpgcreation/PythonSrcCpgGenerator.scala b/console/src/main/scala/io/joern/console/cpgcreation/PythonSrcCpgGenerator.scala
index 3a69dc22e..438b0ecaf 100644
--- a/console/src/main/scala/io/joern/console/cpgcreation/PythonSrcCpgGenerator.scala
+++ b/console/src/main/scala/io/joern/console/cpgcreation/PythonSrcCpgGenerator.scala
@@ -28,6 +28,7 @@ case class PythonSrcCpgGenerator(config: FrontendConfig, rootPath: Path) extends
 
   override def applyPostProcessingPasses(cpg: Cpg): Cpg = {
     new ImportsPass(cpg).createAndApply()
+    new ImportResolverPass(cpg).createAndApply()
     new DynamicTypeHintFullNamePass(cpg).createAndApply()
     new PythonInheritanceNamePass(cpg).createAndApply()
     new PythonTypeRecoveryPass(cpg, XTypeRecoveryConfig(enabledDummyTypes = !pyConfig.forall(_.disableDummyTypes)))
diff --git a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/queryengine/TaskCreator.scala b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/queryengine/TaskCreator.scala
index f6b42b265..27ca32464 100644
--- a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/queryengine/TaskCreator.scala
+++ b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/queryengine/TaskCreator.scala
@@ -2,7 +2,15 @@ package io.joern.dataflowengineoss.queryengine
 
 import io.joern.dataflowengineoss.queryengine.Engine.argToOutputParams
 import io.shiftleft.codepropertygraph.Cpg
-import io.shiftleft.codepropertygraph.generated.nodes.{Call, Expression, MethodParameterIn, MethodParameterOut, Return}
+import io.shiftleft.codepropertygraph.generated.nodes.{
+  Call,
+  Expression,
+  Method,
+  MethodParameterIn,
+  MethodParameterOut,
+  MethodRef,
+  Return
+}
 import io.shiftleft.semanticcpg.language.NoResolve
 import io.shiftleft.semanticcpg.language._
 
@@ -93,49 +101,71 @@ class TaskCreator(context: EngineContext) {
       .distinct
 
     val forCalls = outArgsAndCalls.flatMap { case (result, outArg, path, callDepth) =>
-      val outCall = outArg.collect { case n: Call => n }
-
-      val methodReturns = outCall.toList
-        .flatMap(x => NoResolve.getCalledMethods(x).methodReturn.map(y => (x, y)))
-        .iterator
-
-      methodReturns.flatMap { case (call, methodReturn) =>
-        val method           = methodReturn.method
-        val returnStatements = methodReturn._reachingDefIn.toList.collect { case r: Return => r }
-        if (method.isExternal || method.start.isStub.nonEmpty) {
-          val newPath = path
-          (call.receiver.l ++ call.argument.l).map { arg =>
-            val taskStack = result.taskStack :+ TaskFingerprint(arg, result.callSiteStack, callDepth)
-            ReachableByTask(taskStack, newPath)
-          }
-        } else {
-          returnStatements.map { returnStatement =>
-            val newPath = Vector(PathElement(methodReturn, result.callSiteStack)) ++ path
-            val taskStack =
-              result.taskStack :+ TaskFingerprint(returnStatement, call :: result.callSiteStack, callDepth + 1)
-            ReachableByTask(taskStack, newPath)
+      outArg.toList.flatMap {
+        case call: Call =>
+          val methodReturns = call.toList
+            .flatMap(x => NoResolve.getCalledMethods(x).methodReturn.map(y => (x, y)))
+            .iterator
+
+          methodReturns.flatMap { case (call, methodReturn) =>
+            val method           = methodReturn.method
+            val returnStatements = methodReturn._reachingDefIn.toList.collect { case r: Return => r }
+            if (method.isExternal || method.start.isStub.nonEmpty) {
+              val newPath = path
+              (call.receiver.l ++ call.argument.l).map { arg =>
+                val taskStack = result.taskStack :+ TaskFingerprint(arg, result.callSiteStack, callDepth)
+                ReachableByTask(taskStack, newPath)
+              }
+            } else {
+              returnStatements.map { returnStatement =>
+                val newPath = Vector(PathElement(methodReturn, result.callSiteStack)) ++ path
+                val taskStack =
+                  result.taskStack :+ TaskFingerprint(returnStatement, call :: result.callSiteStack, callDepth + 1)
+                ReachableByTask(taskStack, newPath)
+              }
+            }
           }
-        }
+        case _ => Vector.empty
       }
     }
 
     val forArgs = outArgsAndCalls.flatMap { case (result, args, path, callDepth) =>
-      args.toList.flatMap { case arg: Expression =>
-        val outParams = if (result.callSiteStack.nonEmpty) {
-          List[MethodParameterOut]()
-        } else {
-          argToOutputParams(arg).l
-        }
-        outParams
-          .filterNot(_.method.isExternal)
-          .map { p =>
-            val newStack = arg.inCall.headOption.map { x => x :: result.callSiteStack }.getOrElse(result.callSiteStack)
-            ReachableByTask(result.taskStack :+ TaskFingerprint(p, newStack, callDepth + 1), path)
+      args.toList.flatMap {
+        case arg: Expression =>
+          val outParams = if (result.callSiteStack.nonEmpty) {
+            List[MethodParameterOut]()
+          } else {
+            argToOutputParams(arg).l
+          }
+          outParams
+            .filterNot(_.method.isExternal)
+            .map { p =>
+              val newStack =
+                arg.inCall.headOption.map { x => x :: result.callSiteStack }.getOrElse(result.callSiteStack)
+              ReachableByTask(result.taskStack :+ TaskFingerprint(p, newStack, callDepth + 1), path)
+            }
+        case _ => Vector.empty
+      }
+    }
+
+    val forMethodRefs = outArgsAndCalls.flatMap { case (result, outArg, path, callDepth) =>
+      outArg.toList.flatMap {
+        case methodRef: MethodRef =>
+          val methodReturns = methodRef._refOut.collectAll[Method].methodReturn
+          methodReturns.flatMap { methodReturn =>
+            val returnStatements = methodReturn._reachingDefIn.toList.collect { case r: Return => r }
+            returnStatements.map { returnStatement =>
+              val newPath = Vector(PathElement(methodReturn, result.callSiteStack)) ++ path
+              val taskStack =
+                result.taskStack :+ TaskFingerprint(returnStatement, result.callSiteStack, callDepth + 1)
+              ReachableByTask(taskStack, newPath)
+            }
           }
+        case _ => Vector.empty
       }
     }
 
-    forCalls ++ forArgs
+    forCalls ++ forArgs ++ forMethodRefs
   }
 
 }
diff --git a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/queryengine/TaskSolver.scala b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/queryengine/TaskSolver.scala
index 85317b7ec..263251775 100644
--- a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/queryengine/TaskSolver.scala
+++ b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/queryengine/TaskSolver.scala
@@ -190,6 +190,8 @@ class TaskSolver(task: ReachableByTask, context: EngineContext, sources: Set[Cfg
             && !arg.inCall.headOption.exists(x => isArgOrRetOfMethodWeCameFrom(x, path)) =>
         createPartialResultForOutputArgOrRet()
 
+      case _: MethodRef => createPartialResultForOutputArgOrRet()
+
       // All other cases: expand into parents
       case _ =>
         computeResultsForParents()
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/JsSrc2Cpg.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/JsSrc2Cpg.scala
index ad294dad9..8f446dadb 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/JsSrc2Cpg.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/JsSrc2Cpg.scala
@@ -60,6 +60,7 @@ object JsSrc2Cpg {
     List(
       new JavaScriptInheritanceNamePass(cpg),
       new ConstClosurePass(cpg),
+      new ImportResolverPass(cpg),
       new JavaScriptTypeRecoveryPass(cpg, XTypeRecoveryConfig(enabledDummyTypes = !config.exists(_.disableDummyTypes))),
       new JavaScriptTypeHintCallLinker(cpg),
       new NaiveCallLinker(cpg)
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ImportResolverPass.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ImportResolverPass.scala
new file mode 100644
index 000000000..4bd3b40a3
--- /dev/null
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ImportResolverPass.scala
@@ -0,0 +1,125 @@
+package io.joern.jssrc2cpg.passes
+
+import io.joern.x2cpg.passes.frontend.ImportsPass._
+import io.joern.x2cpg.passes.frontend.XImportResolverPass
+import io.joern.x2cpg.{Defines => XDefines}
+import io.shiftleft.codepropertygraph.Cpg
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, Identifier, Method, MethodRef}
+import io.shiftleft.semanticcpg.language._
+
+import java.io.{File => JFile}
+import java.util.regex.{Matcher, Pattern}
+import scala.util.{Failure, Success, Try}
+
+class ImportResolverPass(cpg: Cpg) extends XImportResolverPass(cpg) {
+
+  private val pathPattern = Pattern.compile("[\"']([\\w/.]+)[\"']")
+
+  override protected def optionalResolveImport(
+    fileName: String,
+    importCall: Call,
+    importedEntity: String,
+    importedAs: String,
+    diffGraph: DiffGraphBuilder
+  ): Unit = {
+    val pathSep     = ":"
+    val rawEntity   = importedEntity.stripPrefix("./")
+    val alias       = importedAs
+    val matcher     = pathPattern.matcher(rawEntity)
+    val sep         = Matcher.quoteReplacement(JFile.separator)
+    val root        = codeRoot + JFile.separator
+    val currentFile = s"$root$fileName"
+    // We want to know if the import is local since if an external name is used to match internal methods we may have
+    // false paths.
+    val isLocalImport = importedEntity.matches("^[.]+/?.*")
+    // TODO: At times there is an operation inside of a require, e.g. path.resolve(__dirname + "/../config/env/all.js")
+    //  this tries to recover the string but does not perform string constant propagation
+    val entity = if (matcher.find()) matcher.group(1) else rawEntity
+    val resolvedPath = better.files
+      .File(currentFile.stripSuffix(currentFile.split(sep).last), entity.split(pathSep).head)
+      .pathAsString
+      .stripPrefix(root)
+
+    val isImportingModule = !entity.contains(pathSep)
+
+    def targetModule = Try(
+      if (isLocalImport)
+        cpg
+          .file(s"${Pattern.quote(resolvedPath)}\\.?.*")
+          .method
+          .nameExact(":program")
+      else
+        Iterator.empty
+    ) match {
+      case Failure(_) =>
+        logger.warn(s"Unable to resolve import due to irregular regex at '$importedEntity'")
+        Iterator.empty
+      case Success(modules) => modules
+    }
+
+    def targetAssignments = targetModule
+      .nameExact(":program")
+      .ast
+      .assignment
+
+    val matchingExports = if (isImportingModule) {
+      // If we are importing the whole module, we need to load all entities
+      targetAssignments
+        .code(s"\\_tmp\\_\\d+\\.\\w+ =.*", "(module\\.)?exports.*")
+        .dedup
+        .l
+    } else {
+      // If we are importing a specific entity, then we look for it here
+      targetAssignments
+        .code("^(module.)?exports.*")
+        .where(_.argument.codeExact(alias))
+        .dedup
+        .l
+    }
+
+    (if (matchingExports.nonEmpty) {
+       matchingExports.flatMap { exp =>
+         exp.argument.l match {
+           case ::(expCall: Call, ::(b: Identifier, _))
+               if expCall.code.matches("^(module.)?exports[.]?.*") && b.name == alias =>
+             val moduleMethods      = targetModule.ast.isMethod.l
+             lazy val methodMatches = moduleMethods.name(b.name).l
+             lazy val constructorMatches =
+               moduleMethods.fullName(s".*${b.name}$pathSep${XDefines.ConstructorMethodName}$$").l
+             lazy val moduleExportsThisVariable = moduleMethods.ast.isLocal
+               .where(_.nameExact(b.name))
+               .nonEmpty
+             // Exported function with only the name of the function
+             val methodPaths =
+               if (methodMatches.nonEmpty) methodMatches.fullName.toSet
+               else constructorMatches.fullName.toSet
+             if (methodPaths.nonEmpty) {
+               methodPaths.flatMap(x => Set(ResolvedMethod(x, alias, Option("this")), ResolvedTypeDecl(x)))
+             } else if (moduleExportsThisVariable) {
+               Set(ResolvedMember(targetModule.fullName.head, b.name))
+             } else {
+               Set.empty
+             }
+           case ::(x: Call, ::(b: MethodRef, _)) =>
+             // Exported function with a method ref of the function
+             val methodName = x.argumentOption(2).map(_.code).getOrElse(b.referencedMethod.name)
+             val (callName, receiver) =
+               if (methodName == "exports") (alias, Option("this")) else (methodName, Option(alias))
+             b.referencedMethod.astParent.iterator
+               .collectAll[Method]
+               .fullName
+               .map(x => ResolvedTypeDecl(x))
+               .toSet ++ Set(ResolvedMethod(b.methodFullName, callName, receiver))
+           case ::(_, ::(y: Call, _)) =>
+             // Exported closure with a method ref within the AST of the RHS
+             y.ast.isMethodRef.map(mRef => ResolvedMethod(mRef.methodFullName, alias, Option("this"))).toSet
+           case _ =>
+             Set.empty[ResolvedImport]
+         }
+       }.toSet
+     } else {
+       Set(UnknownMethod(entity, alias, Option("this")), UnknownTypeDecl(entity))
+     }).foreach(x => resolvedImportToTag(x, importCall, diffGraph))
+  }
+
+}
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ImportsPass.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ImportsPass.scala
index 2204b84a0..220eec82d 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ImportsPass.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ImportsPass.scala
@@ -1,11 +1,11 @@
 package io.joern.jssrc2cpg.passes
 
-import io.joern.x2cpg.Imports.createImportNodeAndLink
 import io.joern.x2cpg.X2Cpg
+import io.joern.x2cpg.passes.frontend.XImportsPass
 import io.shiftleft.codepropertygraph.Cpg
-import io.shiftleft.passes.CpgPass
-import overflowdb.BatchedUpdate
+import io.shiftleft.codepropertygraph.generated.nodes.Call
 import io.shiftleft.semanticcpg.language._
+import io.shiftleft.semanticcpg.language.operatorextension.OpNodes.Assignment
 
 /** This pass creates `IMPORT` nodes by looking for calls to `require`. `IMPORT` nodes are linked to existing dependency
   * nodes, or, if no suitable dependency node exists, a dependency node is created.
@@ -15,17 +15,13 @@ import io.shiftleft.semanticcpg.language._
   *
   * TODO Dependency node creation is still missing.
   */
-class ImportsPass(cpg: Cpg) extends CpgPass(cpg) {
+class ImportsPass(cpg: Cpg) extends XImportsPass(cpg) {
 
-  override def run(diffGraph: BatchedUpdate.DiffGraphBuilder): Unit = {
-    cpg
-      .call("require")
-      .flatMap { x => x.inAssignment.codeNot("var .*").map(y => (x, y)) }
-      .foreach { case (call, assignment) =>
-        val importedAs     = assignment.target.code
-        val importedEntity = X2Cpg.stripQuotes(call.argument(1).code)
-        createImportNodeAndLink(importedEntity, importedAs, Option(call), diffGraph)
-      }
-  }
+  override protected val importCallName: String = "require"
+
+  override protected def importCallToPart(x: Call): Iterator[(Call, Assignment)] =
+    x.inAssignment.codeNot("var .*").map(y => (x, y))
+
+  override protected def importedEntityFromCall(call: Call): String = X2Cpg.stripQuotes(call.argument(1).code)
 
 }
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/JavaScriptTypeRecovery.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/JavaScriptTypeRecovery.scala
index dbba7423d..080708f60 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/JavaScriptTypeRecovery.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/JavaScriptTypeRecovery.scala
@@ -10,10 +10,6 @@ import io.shiftleft.semanticcpg.language._
 import io.shiftleft.semanticcpg.language.operatorextension.OpNodes.FieldAccess
 import overflowdb.BatchedUpdate.DiffGraphBuilder
 
-import java.io.{File => JFile}
-import java.util.regex.{Matcher, Pattern}
-import scala.util.{Failure, Success, Try}
-
 class JavaScriptTypeRecoveryPass(cpg: Cpg, config: XTypeRecoveryConfig = XTypeRecoveryConfig())
     extends XTypeRecoveryPass[File](cpg, config) {
   override protected def generateRecoveryPass(state: XTypeRecoveryState): XTypeRecovery[File] =
@@ -48,8 +44,6 @@ private class RecoverForJavaScriptFile(cpg: Cpg, cu: File, builder: DiffGraphBui
   override protected def isConstructor(name: String): Boolean =
     !name.isBlank && (name.charAt(0).isUpper || name.endsWith("factory"))
 
-  lazy private val pathPattern = Pattern.compile("[\"']([\\w/.]+)[\"']")
-
   override protected def prepopulateSymbolTableEntry(x: AstNode): Unit = x match {
     case x @ (_: Identifier | _: Local | _: MethodParameterIn)
         if x.property(PropertyNames.TYPE_FULL_NAME, Defines.Any) != Defines.Any =>
@@ -96,113 +90,6 @@ private class RecoverForJavaScriptFile(cpg: Cpg, cu: File, builder: DiffGraphBui
     }
   }
 
-  override protected def visitImport(i: Import): Unit = for {
-    rawEntity <- i.importedEntity.map(_.stripPrefix("./"))
-    alias     <- i.importedAs
-  } {
-    val matcher = pathPattern.matcher(rawEntity)
-    val sep     = Matcher.quoteReplacement(JFile.separator)
-    val currentFile = codeRoot + (cu match {
-      case x: File => x.name
-      case _       => cu.file.name.headOption.getOrElse("")
-    })
-    // We want to know if the import is local since if an external name is used to match internal methods we may have
-    // false paths.
-    val isLocalImport = i.importedEntity.exists(_.matches("^[.]+/?.*"))
-    // TODO: At times there is an operation inside of a require, e.g. path.resolve(__dirname + "/../config/env/all.js")
-    //  this tries to recover the string but does not perform string constant propagation
-    val entity = if (matcher.find()) matcher.group(1) else rawEntity
-    val resolvedPath = better.files
-      .File(currentFile.stripSuffix(currentFile.split(sep).last), entity.split(":").head)
-      .pathAsString
-      .stripPrefix(codeRoot)
-
-    val isImportingModule = !entity.contains(":")
-
-    def targetModule = Try(
-      if (isLocalImport)
-        cpg
-          .file(s"${Pattern.quote(resolvedPath)}\\.?.*")
-          .method
-      else
-        Iterator.empty
-    ) match {
-      case Failure(_) =>
-        logger.warn(s"Unable to resolve import due to irregular regex at '${i.importedEntity.getOrElse("")}'")
-        Iterator.empty
-      case Success(modules) => modules
-    }
-
-    def targetAssignments = targetModule
-      .nameExact(":program")
-      .ast
-      .assignment
-
-    val matchingExports = if (isImportingModule) {
-      // If we are importing the whole module, we need to load all entities
-      targetAssignments
-        .code(s"\\_tmp\\_\\d+\\.\\w+ =.*", "(module\\.)?exports.*")
-        .dedup
-        .l
-    } else {
-      // If we are importing a specific entity, then we look for it here
-      targetAssignments
-        .code("^(module.)?exports.*")
-        .where(_.argument.codeExact(alias))
-        .dedup
-        .l
-    }
-
-    if (matchingExports.nonEmpty) {
-      matchingExports.flatMap { exp =>
-        exp.argument.l match {
-          case ::(expCall: Call, ::(b: Identifier, _)) if expCall.code.matches("^(module.)?exports[.]?.*") =>
-            val moduleMethods      = targetModule.ast.isMethod.l
-            lazy val methodMatches = moduleMethods.name(b.name).l
-            lazy val constructorMatches =
-              moduleMethods.fullName(s".*${b.name}$pathSep${XDefines.ConstructorMethodName}$$").l
-            lazy val variableMatches = cpg
-              .file(s"${Pattern.quote(resolvedPath)}\\.?.*")
-              .method
-              .ast
-              .isIdentifier
-              .name(b.name)
-              .flatMap(i => i.typeFullName +: i.dynamicTypeHintFullName)
-              .filterNot(_ == Defines.Any)
-              .toSet
-            // Exported function with only the name of the function
-            val methodPaths =
-              if (methodMatches.nonEmpty) methodMatches.fullName.toSet
-              else constructorMatches.fullName.toSet
-            if (methodPaths.nonEmpty) {
-              symbolTable.append(CallAlias(alias, Option("this")), methodPaths)
-              symbolTable.append(LocalVar(alias), methodPaths)
-            } else {
-              symbolTable.append(LocalVar(alias), variableMatches)
-            }
-          case ::(x: Call, ::(b: MethodRef, _)) =>
-            // Exported function with a method ref of the function
-            val methodName = x.argumentOption(2).map(_.code).getOrElse(b.referencedMethod.name)
-            if (methodName == "exports") symbolTable.append(CallAlias(alias, Option("this")), Set(b.methodFullName))
-            else symbolTable.append(CallAlias(methodName, Option(alias)), Set(b.methodFullName))
-            symbolTable.append(LocalVar(alias), b.referencedMethod.astParent.iterator.collectAll[Method].fullName.toSet)
-          case ::(_, ::(y: Call, _)) =>
-            // Exported closure with a method ref within the AST of the RHS
-            y.ast.isMethodRef.flatMap { mRef =>
-              val methodName = mRef.referencedMethod.name
-              symbolTable.append(CallAlias(methodName, Option(alias)), Set(mRef.methodFullName))
-            }
-          case _ =>
-            Set.empty[String]
-        }
-      }.toSet
-    } else {
-      val default = Set(entity)
-      symbolTable.append(LocalVar(alias), default)
-      symbolTable.append(CallAlias(alias, Option("this")), default)
-    }
-  }
-
   private lazy val exportedIdentifiers = cu.method
     .nameExact(":program")
     .ast
diff --git a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/ImportResolverPass.scala b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/ImportResolverPass.scala
new file mode 100644
index 000000000..e8b3360d7
--- /dev/null
+++ b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/ImportResolverPass.scala
@@ -0,0 +1,140 @@
+package io.joern.pysrc2cpg
+
+import better.files.{File => BFile}
+import io.joern.x2cpg.passes.frontend.ImportsPass._
+import io.joern.x2cpg.passes.frontend.XImportResolverPass
+import io.shiftleft.codepropertygraph.Cpg
+import io.shiftleft.codepropertygraph.generated.nodes._
+import io.shiftleft.semanticcpg.language._
+
+import java.io.{File => JFile}
+import java.util.regex.{Matcher, Pattern}
+
+class ImportResolverPass(cpg: Cpg) extends XImportResolverPass(cpg) {
+
+  override protected def optionalResolveImport(
+    fileName: String,
+    importCall: Call,
+    importedEntity: String,
+    importedAs: String,
+    diffGraph: DiffGraphBuilder
+  ): Unit = {
+    val (namespace, entityName) = if (importedEntity.contains(".")) {
+      val splitName = importedEntity.split('.').toSeq
+      val namespace = importedEntity.stripSuffix(s".${splitName.last}")
+      (relativizeNamespace(namespace, fileName), splitName.last)
+    } else {
+      ("", importedEntity)
+    }
+
+    resolveEntities(namespace, entityName, importedAs).foreach(x => resolvedImportToTag(x, importCall, diffGraph))
+  }
+
+  private def relativizeNamespace(path: String, fileName: String): String = if (path.startsWith(".")) {
+    // TODO: pysrc2cpg does not link files to the correct namespace nodes
+    val sep = Matcher.quoteReplacement(JFile.separator)
+    // The below gives us the full path of the relative "."
+    val relativeNamespace =
+      if (fileName.contains(JFile.separator))
+        fileName.substring(0, fileName.lastIndexOf(JFile.separator)).replaceAll(sep, ".")
+      else ""
+    (if (path.length > 1) relativeNamespace + path.replaceAll(sep, ".")
+     else relativeNamespace).stripPrefix(".")
+  } else path
+
+  /** For an import - given by its module path and the name of the imported function or module - determine the possible
+    * callee names.
+    *
+    * @param path
+    *   the module path.
+    * @param expEntity
+    *   the name of the imported entity. This could be a function, module, or variable/field.
+    * @param alias
+    *   how the imported entity is named.
+    * @return
+    *   the possible callee names
+    */
+  private def resolveEntities(path: String, expEntity: String, alias: String): Set[ResolvedImport] = {
+
+    implicit class ResolvedNodeExt(val traversal: Seq[String]) {
+      def toResolvedImport(cpg: Cpg): Seq[ResolvedImport] = {
+        val resolvedEntities =
+          traversal.flatMap(x => cpg.typeDecl.fullNameExact(x) ++ cpg.method.fullNameExact(x)).collect {
+            case x: Method   => ResolvedMethod(x.fullName, alias)
+            case x: TypeDecl => ResolvedTypeDecl(x.fullName)
+          }
+        if (resolvedEntities.isEmpty) {
+          traversal.filterNot(_.contains("__init__.py")).map(x => UnknownImport(x))
+        } else {
+          resolvedEntities
+        }
+      }
+    }
+
+    implicit class CalleeAsInitExt(val name: String) {
+      def asInit: String = if (name.contains("__init__.py")) name
+      else name.replace(".py", s"${JFile.separator}__init__.py")
+
+      def withInit: Seq[String] = Seq(name, name.asInit)
+    }
+
+    val pathSep            = "."
+    val sep                = Matcher.quoteReplacement(JFile.separator)
+    val isMaybeConstructor = expEntity.split("\\.").lastOption.exists(s => s.nonEmpty && s.charAt(0).isUpper)
+
+    lazy val membersMatchingImports: List[(TypeDecl, Member)] = cpg.typeDecl
+      .fullName(s".*${Pattern.quote(path)}.*")
+      .flatMap(t =>
+        t.member.nameExact(expEntity).headOption match {
+          case Some(member) => Option((t, member))
+          case None         => None
+        }
+      )
+      .toList
+
+    (path match {
+      case "" if expEntity.contains(".") =>
+        // Case 1: Qualified path: import foo.bar => (bar.py or bar/__init__.py)
+        val splitFunc = expEntity.split("\\.")
+        val name      = splitFunc.tail.mkString(".")
+        s"${splitFunc(0)}.py:<module>$pathSep$name".withInit.toResolvedImport(cpg)
+      case "" =>
+        // Case 2: import of a module: import foo => (foo.py or foo/__init__.py)
+        s"$expEntity.py:<module>".withInit.toResolvedImport(cpg)
+      case _ if membersMatchingImports.nonEmpty =>
+        // Case 3: import of a variable: from api import db => (api.py or foo.__init__.py) @ identifier(db)
+        membersMatchingImports.map {
+          case (t, m) if t.method.nameExact(m.name).nonEmpty =>
+            ResolvedMethod(t.method.nameExact(m.name).fullName.head, alias)
+          case (t, m) if t.astSiblings.isMethod.fullNameExact(t.fullName).ast.isTypeDecl.nameExact(m.name).nonEmpty =>
+            ResolvedTypeDecl(
+              t.astSiblings.isMethod.fullNameExact(t.fullName).ast.isTypeDecl.nameExact(m.name).fullName.head
+            )
+          case (t, m) => ResolvedMember(t.fullName, m.name)
+        }
+      case _ =>
+        // Case 4:  Import from module using alias, e.g. import bar from foo as faz
+        val fileOrDir = BFile(codeRoot) / path
+        val pyFile    = BFile(codeRoot) / s"$path.py"
+        fileOrDir match {
+          case f if f.isDirectory && !pyFile.exists =>
+            Seq(s"${path.replaceAll("\\.", sep)}${java.io.File.separator}$expEntity.py:<module>").toResolvedImport(cpg)
+          case f if f.isDirectory && (f / s"$expEntity.py").exists =>
+            Seq(s"${(f / s"$expEntity.py").pathAsString.stripPrefix(codeRoot)}:<module>").toResolvedImport(cpg)
+          case _ =>
+            s"${path.replaceAll("\\.", sep)}.py:<module>$pathSep$expEntity".withInit.toResolvedImport(cpg)
+        }
+    }).flatMap {
+      // If we import the constructor, we also import the type
+      case x: ResolvedMethod if isMaybeConstructor =>
+        Seq(ResolvedMethod(Seq(x.fullName, "__init__").mkString(pathSep), alias), ResolvedTypeDecl(x.fullName))
+      // If we import the type, we also import the constructor
+      case x: ResolvedTypeDecl if isMaybeConstructor =>
+        Seq(x, ResolvedMethod(Seq(x.fullName, "__init__").mkString(pathSep), alias))
+      // If we can determine the import is a constructor, then it is likely not a member
+      case x: UnknownImport if isMaybeConstructor =>
+        Seq(UnknownMethod(Seq(x.path, "__init__").mkString(pathSep), alias), UnknownTypeDecl(x.path))
+      case x => Seq(x)
+    }.toSet
+  }
+}
diff --git a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/ImportsPass.scala b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/ImportsPass.scala
index 3f98873fe..3c6f292c7 100644
--- a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/ImportsPass.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/ImportsPass.scala
@@ -1,28 +1,18 @@
 package io.joern.pysrc2cpg
 
-import io.joern.x2cpg.Imports.createImportNodeAndLink
+import io.joern.x2cpg.passes.frontend.XImportsPass
 import io.shiftleft.codepropertygraph.Cpg
-import io.shiftleft.codepropertygraph.generated.nodes.Call
-import io.shiftleft.passes.CpgPass
-import overflowdb.BatchedUpdate
+import io.shiftleft.codepropertygraph.generated.nodes._
 import io.shiftleft.semanticcpg.language._
+import io.shiftleft.semanticcpg.language.operatorextension.OpNodes.Assignment
 
-class ImportsPass(cpg: Cpg) extends CpgPass(cpg) {
-  override def run(diffGraph: BatchedUpdate.DiffGraphBuilder): Unit = {
-    val importsAndAssignments = cpg
-      .call("import")
-      .flatMap { x =>
-        x.inAssignment.map(y => (x, y))
-      }
+class ImportsPass(cpg: Cpg) extends XImportsPass(cpg) {
 
-    importsAndAssignments.foreach { case (call, assignment) =>
-      val importedEntity = importedEntityFromCall(call)
-      val importedAs     = assignment.target.code
-      createImportNodeAndLink(importedEntity, importedAs, Some(call), diffGraph)
-    }
-  }
+  override protected val importCallName: String = "import"
+
+  override protected def importCallToPart(x: Call): Iterator[(Call, Assignment)] = x.inAssignment.map(y => (x, y))
 
-  def importedEntityFromCall(call: Call): String = {
+  override def importedEntityFromCall(call: Call): String = {
     call.argument.code.l match {
       case List("", what)       => what
       case List(where, what)    => s"$where.$what"
diff --git a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonTypeRecovery.scala b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonTypeRecovery.scala
index 495a181fc..3e8708bd8 100644
--- a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonTypeRecovery.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonTypeRecovery.scala
@@ -1,6 +1,5 @@
 package io.joern.pysrc2cpg
 
-import better.files.{File => BFile}
 import io.joern.x2cpg.passes.frontend._
 import io.shiftleft.codepropertygraph.Cpg
 import io.shiftleft.codepropertygraph.generated.nodes._
@@ -10,9 +9,6 @@ import io.shiftleft.semanticcpg.language.operatorextension.OpNodes
 import io.shiftleft.semanticcpg.language.operatorextension.OpNodes.FieldAccess
 import overflowdb.BatchedUpdate.DiffGraphBuilder
 
-import java.io.{File => JFile}
-import java.util.regex.{Matcher, Pattern}
-
 class PythonTypeRecoveryPass(cpg: Cpg, config: XTypeRecoveryConfig = XTypeRecoveryConfig())
     extends XTypeRecoveryPass[File](cpg, config) {
 
@@ -51,36 +47,30 @@ private class RecoverForPythonFile(cpg: Cpg, cu: File, builder: DiffGraphBuilder
   override val symbolTable: SymbolTable[LocalKey] = new SymbolTable[LocalKey](fromNodeToLocalPythonKey)
 
   override def visitImport(i: Import): Unit = {
-
-    def relativizeNamespace(path: String) = if (path.startsWith(".")) {
-      // TODO: pysrc2cpg does not link files to the correct namespace nodes
-      val fileName = i.file.name.headOption.getOrElse("").stripPrefix(codeRoot)
-      val sep      = Matcher.quoteReplacement(JFile.separator)
-      // The below gives us the full path of the relative "."
-      val relativeNamespace =
-        if (fileName.contains(JFile.separator))
-          fileName.substring(0, fileName.lastIndexOf(JFile.separator)).replaceAll(sep, ".")
-        else ""
-      (if (path.length > 1) relativeNamespace + path.replaceAll(sep, ".")
-       else relativeNamespace).stripPrefix(".")
-    } else path
-
-    val importedEntity = i.importedEntity.getOrElse("")
-    val (namespace, entityName) = if (importedEntity.contains(".")) {
-      val splitName = importedEntity.split('.').toSeq
-      val namespace = importedEntity.stripSuffix(s".${splitName.last}")
-      (relativizeNamespace(namespace), splitName.last)
-    } else {
-      ("", importedEntity)
-    }
-    val calleeNames = extractPossibleCalleeNames(namespace, entityName)
-    i.importedAs match {
-      case Some(alias) =>
-        symbolTable.put(CallAlias(alias), calleeNames)
-        symbolTable.put(LocalVar(alias), calleeNames.map(_.stripSuffix(s"${pathSep}__init__")))
-      case None =>
-        symbolTable.put(CallAlias(entityName), calleeNames)
-        symbolTable.put(LocalVar(entityName), calleeNames.map(_.stripSuffix(s"${pathSep}__init__")))
+    if (i.importedAs.isDefined && i.importedEntity.isDefined) {
+      import io.joern.x2cpg.passes.frontend.ImportsPass._
+
+      val entityName = i.importedAs.get
+      i.call.tag.flatMap(ResolvedImport.tagToResolvedImport).foreach {
+        case ResolvedMethod(fullName, alias, receiver, _) => symbolTable.put(CallAlias(alias, receiver), fullName)
+        case ResolvedTypeDecl(fullName, _)                => symbolTable.put(LocalVar(entityName), fullName)
+        case ResolvedMember(basePath, memberName, _) =>
+          val memberTypes = cpg.typeDecl
+            .fullNameExact(basePath)
+            .member
+            .nameExact(memberName)
+            .flatMap(m => m.typeFullName +: m.dynamicTypeHintFullName)
+            .filterNot(_ == "ANY")
+            .toSet
+          symbolTable.put(LocalVar(entityName), memberTypes)
+        case UnknownMethod(fullName, alias, receiver, _) =>
+          symbolTable.put(CallAlias(alias, receiver), fullName)
+        case UnknownTypeDecl(fullName, _) =>
+          symbolTable.put(LocalVar(entityName), fullName)
+        case UnknownImport(path, _) =>
+          symbolTable.put(CallAlias(entityName), path)
+          symbolTable.put(LocalVar(entityName), path)
+      }
     }
   }
 
@@ -96,105 +86,6 @@ private class RecoverForPythonFile(cpg: Cpg, cu: File, builder: DiffGraphBuilder
     }
   }
 
-  /** For an import - given by its module path and the name of the imported function or module - determine the possible
-    * callee names.
-    *
-    * @param path
-    *   the module path.
-    * @param expEntity
-    *   the name of the imported entity. This could be a function, module, or variable/field.
-    * @return
-    *   the possible callee names
-    */
-  private def extractPossibleCalleeNames(path: String, expEntity: String): Set[String] = {
-    val sep = Matcher.quoteReplacement(JFile.separator)
-
-    lazy val methodsWithExportEntityAsIdentifier: List[String] = cpg.typeDecl
-      .fullName(s".*${Pattern.quote(path)}.*")
-      .where(_.member.nameExact(expEntity))
-      .fullName
-      .toList
-
-    val procedureName = path match {
-      case "" if expEntity.contains(".") =>
-        // Case 1: Qualified path: import foo.bar => (bar.py or bar/__init__.py)
-        val splitFunc = expEntity.split("\\.")
-        val name      = splitFunc.tail.mkString(".")
-        s"${splitFunc(0)}.py:<module>$pathSep$name"
-      case "" =>
-        // Case 2: import of a module: import foo => (foo.py or foo/__init__.py)
-        s"$expEntity.py:<module>"
-      case _ if methodsWithExportEntityAsIdentifier.nonEmpty =>
-        // Case 3: import of a variable: from api import db => (api.py or foo.__init__.py) @ identifier(db)
-        methodsWithExportEntityAsIdentifier.map(f => s"$f<var>$expEntity").head
-      case _ =>
-        // Case 4:  Import from module using alias, e.g. import bar from foo as faz
-        val fileOrDir = BFile(codeRoot) / path
-        val pyFile    = BFile(codeRoot) / s"$path.py"
-        fileOrDir match {
-          case f if f.isDirectory && !pyFile.exists =>
-            s"${path.replaceAll("\\.", sep)}${java.io.File.separator}$expEntity.py:<module>"
-          case f if f.isDirectory && (f / s"$expEntity.py").exists =>
-            s"${(f / s"$expEntity.py").pathAsString}:<module>"
-          case _ =>
-            s"${path.replaceAll("\\.", sep)}.py:<module>$pathSep$expEntity"
-        }
-    }
-
-    /** The two ways that this procedure could be resolved to in Python. */
-    def possibleCalleeNames(procedureName: String, isMaybeConstructor: Boolean, isFieldOrVar: Boolean): Set[String] = {
-      val pythonicFormGuesses =
-        cpg.typeDecl.fullName(s".*${Pattern.quote(procedureName)}").fullName.toSet match {
-          case xs if xs.nonEmpty => xs
-          case _                 => Seq(procedureName)
-        }
-      if (isMaybeConstructor)
-        pythonicFormGuesses.map(p => Seq(p, "__init__").mkString(pathSep.toString)).toSet
-      else if (isFieldOrVar) {
-        val Array(m, v) = procedureName.split("<var>")
-        cpg.typeDecl.fullNameExact(m).member.nameExact(v).headOption match {
-          case Some(i) => (i.typeFullName +: i.dynamicTypeHintFullName).filterNot(_ == Constants.ANY).toSet
-          case None    => Set.empty
-        }
-      } else
-        Set(procedureName, fullNameAsInit) ++ pythonicFormGuesses
-    }
-
-    /** the full name of the procedure where it's assumed that it is defined within an <code>__init.py__</code> of the
-      * module.
-      */
-    def fullNameAsInit: String =
-      if (procedureName.contains("__init__.py")) procedureName
-      else procedureName.replace(".py", s"${JFile.separator}__init__.py")
-
-    val isMaybeConstructor = expEntity.split("\\.").lastOption.exists(s => s.nonEmpty && s.charAt(0).isUpper)
-    possibleCalleeNames(procedureName, isMaybeConstructor, procedureName.contains("<var>"))
-      .map(_.replaceAll("<var>", pathSep.toString))
-  }
-
-  override def postVisitImports(): Unit = {
-    symbolTable.view.foreach { case (k, v) =>
-      val ms = cpg.method.fullNameExact(v.toSeq: _*).l
-      val ts = cpg.typeDecl.fullNameExact(v.toSeq: _*).l
-      // In case a method has been incorrectly determined to be a constructor based on the heuristic
-
-      val nonConstructorV = v.toSeq
-        .map(_.replaceAll("<var>", pathSep.toString).stripSuffix(s"${pathSep}__init__"))
-      val tsNonConstructor = cpg.method.fullNameExact(nonConstructorV: _*).l
-
-      if (ts.nonEmpty)
-        symbolTable.put(k, ts.fullName.toSet)
-      else if (ms.nonEmpty)
-        symbolTable.put(k, ms.fullName.toSet)
-      else if (!tsNonConstructor.forall(_.name == "<module>"))
-        symbolTable.put(k, tsNonConstructor.fullName.toSet)
-      else {
-        // This is likely external and we will ignore the init variant to be consistent
-        symbolTable.put(k, symbolTable(k).filterNot(_.contains("__init__.py")))
-      }
-    }
-  }
-
   /** Determines if a function call is a constructor by following the heuristic that Python classes are typically
     * camel-case and start with an upper-case character.
     */
diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/SymbolTable.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/SymbolTable.scala
index cbbe9bd15..a9db3a8d0 100644
--- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/SymbolTable.scala
+++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/SymbolTable.scala
@@ -100,6 +100,9 @@ class SymbolTable[K <: SBKey](val keyFromNode: AstNode => Option[K]) {
   def append(node: AstNode, typeFullName: String): Set[String] =
     append(node, Set(typeFullName))
 
+  def append(node: K, typeFullName: String): Set[String] =
+    append(node, Set(typeFullName))
+
   def append(node: AstNode, typeFullNames: Set[String]): Set[String] = keyFromNode(node) match {
     case Some(key) => append(key, typeFullNames)
     case None      => Set.empty
diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XImportResolverPass.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XImportResolverPass.scala
new file mode 100644
index 000000000..9d6674ca1
--- /dev/null
+++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XImportResolverPass.scala
@@ -0,0 +1,127 @@
+package io.joern.x2cpg.passes.frontend
+
+import io.joern.x2cpg.passes.frontend.ImportsPass.ResolvedImport
+import io.joern.x2cpg.passes.frontend.ImportsPass.ResolvedImport._
+import io.shiftleft.codepropertygraph.Cpg
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, Import, Tag}
+import io.shiftleft.passes.ConcurrentWriterCpgPass
+import io.shiftleft.semanticcpg.language._
+import org.slf4j.{Logger, LoggerFactory}
+
+import java.io.{File => JFile}
+
+abstract class XImportResolverPass(cpg: Cpg) extends ConcurrentWriterCpgPass[Import](cpg) {
+
+  protected val logger: Logger   = LoggerFactory.getLogger(this.getClass)
+  protected val codeRoot: String = cpg.metaData.root.headOption.getOrElse(JFile.separator)
+
+  override def generateParts(): Array[Import] = cpg.imports.toArray
+
+  override def runOnPart(builder: DiffGraphBuilder, part: Import): Unit = for {
+    call <- part.call
+    fileName = call.file.name.headOption.getOrElse("<unknown>").stripPrefix(codeRoot)
+    importedAs     <- part.importedAs
+    importedEntity <- part.importedEntity
+  } {
+    optionalResolveImport(fileName, call, importedEntity, importedAs, builder)
+  }
+
+  protected def optionalResolveImport(
+    fileName: String,
+    importCall: Call,
+    importedEntity: String,
+    importedAs: String,
+    diffGraph: DiffGraphBuilder
+  ): Unit
+
+  protected def resolvedImportToTag(x: ResolvedImport, importCall: Call, diffGraph: DiffGraphBuilder): Unit =
+    importCall.start.newTagNodePair(x.label, x.serialize).store()(diffGraph)
+
+}
+
+object ImportsPass {
+
+  sealed trait ResolvedImport {
+    def label: String
+
+    def serialize: String
+  }
+
+  implicit class TagToResolvedImportExt(traversal: Traversal[Tag]) {
+    def toResolvedImport: Traversal[ResolvedImport] =
+      traversal.flatMap(ResolvedImport.tagToResolvedImport)
+  }
+
+  object ResolvedImport {
+
+    val RESOLVED_METHOD    = "RESOLVED_METHOD"
+    val RESOLVED_TYPE_DECL = "RESOLVED_TYPE_DECL"
+    val RESOLVED_MEMBER    = "RESOLVED_MEMBER"
+    val UNKNOWN_METHOD     = "UNKNOWN_METHOD"
+    val UNKNOWN_TYPE_DECL  = "UNKNOWN_TYPE_DECL"
+    val UNKNOWN_IMPORT     = "UNKNOWN_IMPORT"
+
+    val OPT_FULL_NAME = "FULL_NAME"
+    val OPT_ALIAS     = "ALIAS"
+    val OPT_RECEIVER  = "RECEIVER"
+    val OPT_BASE_PATH = "BASE_PATH"
+    val OPT_NAME      = "NAME"
+
+    def tagToResolvedImport(tag: Tag): Option[ResolvedImport] = Option(tag.name match {
+      case RESOLVED_METHOD =>
+        val opts = valueToOptions(tag.value)
+        ResolvedMethod(opts(OPT_FULL_NAME), opts(OPT_ALIAS), opts.get(OPT_RECEIVER))
+      case RESOLVED_TYPE_DECL => ResolvedTypeDecl(tag.value)
+      case RESOLVED_MEMBER =>
+        val opts = valueToOptions(tag.value)
+        ResolvedMember(opts(OPT_BASE_PATH), opts(OPT_NAME))
+      case UNKNOWN_METHOD =>
+        val opts = valueToOptions(tag.value)
+        UnknownMethod(opts(OPT_FULL_NAME), opts(OPT_ALIAS), opts.get(OPT_RECEIVER))
+      case UNKNOWN_TYPE_DECL => UnknownTypeDecl(tag.value)
+      case UNKNOWN_IMPORT    => UnknownImport(tag.value)
+      case _                 => null
+    })
+
+    private def valueToOptions(x: String): Map[String, String] =
+      x.split(',').grouped(2).map(xs => xs(0) -> xs(1)).toMap
+  }
+
+  case class ResolvedMethod(
+    fullName: String,
+    alias: String,
+    receiver: Option[String] = None,
+    override val label: String = RESOLVED_METHOD
+  ) extends ResolvedImport {
+    override def serialize: String =
+      s"$OPT_FULL_NAME,$fullName,$OPT_ALIAS,$alias" + receiver.map(r => s",$OPT_RECEIVER,$r").getOrElse("")
+  }
+
+  case class ResolvedTypeDecl(fullName: String, override val label: String = RESOLVED_TYPE_DECL)
+      extends ResolvedImport {
+    override def serialize: String = fullName
+  }
+
+  case class ResolvedMember(basePath: String, memberName: String, override val label: String = RESOLVED_MEMBER)
+      extends ResolvedImport {
+    override def serialize: String = s"$OPT_BASE_PATH,$basePath,$OPT_NAME,$memberName"
+  }
+
+  case class UnknownMethod(
+    fullName: String,
+    alias: String,
+    receiver: Option[String] = None,
+    override val label: String = UNKNOWN_METHOD
+  ) extends ResolvedImport {
+    override def serialize: String =
+      s"$OPT_FULL_NAME,$fullName,$OPT_ALIAS,$alias" + receiver.map(r => s",$OPT_RECEIVER,$r").getOrElse("")
+  }
+
+  case class UnknownTypeDecl(fullName: String, override val label: String = UNKNOWN_TYPE_DECL) extends ResolvedImport {
+    override def serialize: String = fullName
+  }
+
+  case class UnknownImport(path: String, override val label: String = UNKNOWN_IMPORT) extends ResolvedImport {
+    override def serialize: String = path
+  }
+}
diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XImportsPass.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XImportsPass.scala
new file mode 100644
index 000000000..563a1b0dd
--- /dev/null
+++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XImportsPass.scala
@@ -0,0 +1,30 @@
+package io.joern.x2cpg.passes.frontend
+
+import io.joern.x2cpg.Imports.createImportNodeAndLink
+import io.shiftleft.codepropertygraph.Cpg
+import io.shiftleft.codepropertygraph.generated.nodes.Call
+import io.shiftleft.passes.ConcurrentWriterCpgPass
+import io.shiftleft.semanticcpg.language._
+import io.shiftleft.semanticcpg.language.operatorextension.OpNodes.Assignment
+
+abstract class XImportsPass(cpg: Cpg) extends ConcurrentWriterCpgPass[(Call, Assignment)](cpg) {
+
+  protected val importCallName: String
+
+  override def generateParts(): Array[(Call, Assignment)] = cpg
+    .call(importCallName)
+    .flatMap(importCallToPart)
+    .toArray
+
+  protected def importCallToPart(x: Call): Iterator[(Call, Assignment)]
+
+  override def runOnPart(diffGraph: DiffGraphBuilder, part: (Call, Assignment)): Unit = {
+    val (call, assignment) = part
+    val importedEntity     = importedEntityFromCall(call)
+    val importedAs         = assignment.target.code
+    createImportNodeAndLink(importedEntity, importedAs, Some(call), diffGraph)
+  }
+
+  protected def importedEntityFromCall(call: Call): String
+
+}
diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XTypeRecovery.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XTypeRecovery.scala
index 8865616bd..72d3a786d 100644
--- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XTypeRecovery.scala
+++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XTypeRecovery.scala
@@ -266,11 +266,36 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](
   /** Visits an import and stores references in the symbol table as both an identifier and call.
     */
   protected def visitImport(i: Import): Unit = for {
-    entity <- i.importedEntity
-    alias  <- i.importedAs
+    resolvedImport <- i.call.tag
+    alias          <- i.importedAs
   } {
-    symbolTable.append(LocalVar(alias), Set(entity))
-    symbolTable.append(CallAlias(alias), Set(entity))
+    import io.joern.x2cpg.passes.frontend.ImportsPass._
+
+    ResolvedImport.tagToResolvedImport(resolvedImport).foreach {
+      case ResolvedMethod(fullName, alias, receiver, _) =>
+        symbolTable.append(CallAlias(alias, receiver), fullName)
+      case ResolvedTypeDecl(fullName, _) =>
+        symbolTable.append(LocalVar(alias), fullName)
+      case ResolvedMember(basePath, memberName, _) =>
+        val matchingIdentifiers = cpg.method.fullNameExact(basePath).local
+        val matchingMembers     = cpg.typeDecl.fullNameExact(basePath).member
+        val memberTypes = (matchingMembers ++ matchingIdentifiers)
+          .nameExact(memberName)
+          .flatMap(x =>
+            x.property(PropertyNames.TYPE_FULL_NAME, "ANY") +:
+              x.property(PropertyNames.DYNAMIC_TYPE_HINT_FULL_NAME, Seq.empty)
+          )
+          .filterNot(_ == "ANY")
+          .toSet
+        symbolTable.append(LocalVar(alias), memberTypes)
+      case UnknownMethod(fullName, alias, receiver, _) =>
+        symbolTable.append(CallAlias(alias, receiver), fullName)
+      case UnknownTypeDecl(fullName, _) =>
+        symbolTable.append(LocalVar(alias), fullName)
+      case UnknownImport(path, _) =>
+        symbolTable.append(CallAlias(alias), path)
+        symbolTable.append(LocalVar(alias), path)
+    }
   }
 
   /** The initial import setting is over-approximated, so this step checks the CPG for any matches and prunes against


Test patch:
diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/dataflow/LambdaTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/dataflow/LambdaTests.scala
index 17f5af8a5..2ae535439 100644
--- a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/dataflow/LambdaTests.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/dataflow/LambdaTests.scala
@@ -4,7 +4,7 @@ import io.joern.dataflowengineoss.language._
 import io.joern.javasrc2cpg.testfixtures.JavaSrcCode2CpgFixture
 import io.shiftleft.semanticcpg.language._
 
-class LambdaTests extends JavaSrcCode2CpgFixture {
+class LambdaTests extends JavaSrcCode2CpgFixture(withOssDataflow = true) {
 
   "dataflow through a simple lambda call should be found" in {
     val cpg = code("""
@@ -33,4 +33,79 @@ class LambdaTests extends JavaSrcCode2CpgFixture {
     }
   }
 
+  "dataflow around lambda in map" should {
+    val cpg = code("""
+        |import java.util.ArrayList;
+        |import java.util.List;
+        |
+        |public class LambdaExample {
+        |
+        |    public foo1() {
+        |       String myValue = "abc";
+        |		List<String> userPayload = new ArrayList<>();
+        |		List<String> userNamesList = userPayload.stream.map(item -> {
+        |           sink2(myValue);
+        |           return item + myValue;
+        |       });
+        |		sink1(userNamesList);
+        |       return;
+        |    }
+        |}
+        |""".stripMargin)
+
+    "be found for case 1" in {
+      def source = cpg.identifier("item")
+
+      def sink = cpg.call("sink1")
+
+      sink.reachableByFlows(source).isEmpty shouldBe false
+    }
+
+    "be found for case 2" in {
+      def source = cpg.identifier("myValue").head
+
+      def sink = cpg.call("sink2")
+
+      sink.reachableByFlows(source).isEmpty shouldBe false
+    }
+  }
+
+  "dataflow around lambda in foreach" should {
+    val cpg = code("""
+        |import java.util.ArrayList;
+        |import java.util.List;
+        |
+        |public class LambdaExample {
+        |
+        |    public foo() {
+        |       String myValue = "abc";
+        |       List<String> userPayload = new ArrayList<>();
+        |       List<String> userNamesList = new ArrayList<>();
+        |       userPayload.forEach(item -> {
+        |           userNamesList.add(item + myValue);
+        |           sink2(myValue);
+        |       });
+        |       sink1(userNamesList);
+        |       return;
+        |     }
+        |}
+        |""".stripMargin)
+
+    "be found for case 1" ignore {
+      def source = cpg.identifier("item")
+
+      def sink = cpg.call("sink1")
+
+      sink.reachableByFlows(source).isEmpty shouldBe false
+    }
+
+    "be found for case 2" in {
+      def source = cpg.identifier("myValue").head
+
+      def sink = cpg.call("sink2")
+
+      sink.reachableByFlows(source).isEmpty shouldBe false
+    }
+  }
+
 }
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/TypeRecoveryPassTests.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/TypeRecoveryPassTests.scala
index 14ae3dfaf..0d2399c33 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/TypeRecoveryPassTests.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/TypeRecoveryPassTests.scala
@@ -1,6 +1,7 @@
 package io.joern.jssrc2cpg.passes
 
 import io.joern.jssrc2cpg.testfixtures.DataFlowCodeToCpgSuite
+import io.joern.x2cpg.passes.frontend.ImportsPass._
 import io.shiftleft.semanticcpg.language._
 
 class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
@@ -55,6 +56,15 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
       "Test1.ts"
     ).cpg
 
+    "resolve correct imports via tag nodes" in {
+      val List(a: UnknownMethod, b: UnknownTypeDecl, x: UnknownMethod, y: UnknownTypeDecl) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      a.fullName shouldBe "slack_sdk:WebClient"
+      b.fullName shouldBe "slack_sdk:WebClient"
+      x.fullName shouldBe "sendgrid:SendGridAPIClient"
+      y.fullName shouldBe "sendgrid:SendGridAPIClient"
+    }
+
     "resolve 'sg' identifier types from import information" in {
       val List(sg1, sg2, sg3) = cpg.identifier.nameExact("sg").l
       sg1.typeFullName shouldBe "sendgrid:SendGridAPIClient"
@@ -130,6 +140,19 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
       "Bar.ts"
     ).cpg
 
+    "resolve correct imports via tag nodes" in {
+      val List(a: ResolvedMember, b: ResolvedMember, c: ResolvedMember, d: UnknownMethod, e: UnknownTypeDecl) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      a.basePath shouldBe "Foo.ts::program"
+      a.memberName shouldBe "x"
+      b.basePath shouldBe "Foo.ts::program"
+      b.memberName shouldBe "y"
+      c.basePath shouldBe "Foo.ts::program"
+      c.memberName shouldBe "db"
+      d.fullName shouldBe "flask_sqlalchemy:SQLAlchemy"
+      e.fullName shouldBe "flask_sqlalchemy:SQLAlchemy"
+    }
+
     "resolve 'x' and 'y' locally under foo.py" in {
       val Some(x) = cpg.file.name(".*Foo.*").ast.isIdentifier.nameExact("x").headOption
       x.typeFullName shouldBe "__ecma.Number"
@@ -214,6 +237,11 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
       "foo.js"
     )
 
+    "resolve correct imports via tag nodes" in {
+      val List(x: ResolvedMethod) = cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      x.fullName shouldBe "util.js::program:getIncrementalInteger"
+    }
+
     "resolve the method full name off of an aliased 'this'" in {
       val Some(x) = cpg.file("util.js").ast.isCall.nameExact("getIncrementalInteger").headOption
       x.methodFullName shouldBe "util.js::program:getIncrementalInteger"
@@ -237,6 +265,12 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
         |const driveObj = google.drive({ version: 'v3', auth });
         |""".stripMargin)
 
+    "resolve correct imports via tag nodes" in {
+      val List(x: UnknownMethod, y: UnknownTypeDecl) = cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      x.fullName shouldBe "googleapis"
+      y.fullName shouldBe "googleapis"
+    }
+
     "be propagated to `methodFullName` of call" in {
       val List(methodFullName) = cpg.call.code("google.drive\\(.*").methodFullName.l
       methodFullName shouldBe "googleapis:drive"
@@ -252,6 +286,14 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
         |const driveObj = google.drive({ version: 'v3', auth });
         |""".stripMargin)
 
+    "resolve correct imports via tag nodes" in {
+      val List(x: UnknownMethod, y: UnknownTypeDecl, z: UnknownMethod) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      x.fullName shouldBe "googleapis"
+      y.fullName shouldBe "googleapis"
+      z.fullName shouldBe "googleapis"
+    }
+
     "be propagated to `methodFullName` of call" in {
       val List(methodFullName) = cpg.call.code("google.drive\\(.*").methodFullName.l
       methodFullName shouldBe "googleapis:drive"
@@ -343,6 +385,17 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
       "bar.js"
     )
 
+    "resolve correct imports via tag nodes" in {
+      val List(a: ResolvedTypeDecl, b: ResolvedMethod, c: ResolvedMethod, d: UnknownMethod, e: UnknownTypeDecl) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      a.fullName shouldBe "foo.js::program"
+      b.fullName shouldBe "foo.js::program:literalFunction"
+      c.fullName shouldBe "foo.js::program:get"
+      d.fullName shouldBe "axios"
+      e.fullName shouldBe "axios"
+
+    }
+
     "propagate literal types to the method return" in {
       val Some(literalMethod) = cpg.method.nameExact("literalFunction").headOption
       literalMethod.methodReturn.typeFullName shouldBe "__ecma.Number"
diff --git a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/PySrc2CpgFixture.scala b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/PySrc2CpgFixture.scala
index 730cdd1d0..16ac4b3a4 100644
--- a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/PySrc2CpgFixture.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/PySrc2CpgFixture.scala
@@ -36,6 +36,7 @@ class PySrcTestCpg extends TestCpg with PythonFrontend {
   override def applyPasses(): Unit = {
     X2Cpg.applyDefaultOverlays(this)
     new ImportsPass(this).createAndApply()
+    new ImportResolverPass(this).createAndApply()
     new PythonInheritanceNamePass(this).createAndApply()
     new DynamicTypeHintFullNamePass(this).createAndApply()
     new PythonTypeRecoveryPass(this).createAndApply()
diff --git a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala
index 378c81833..81d2a8731 100644
--- a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala
@@ -7,6 +7,8 @@ import io.shiftleft.codepropertygraph.Cpg
 import io.shiftleft.codepropertygraph.generated.nodes.{Literal, Member, Method}
 import io.shiftleft.semanticcpg.language._
 
+import java.io.File
+
 class DataFlowTests extends PySrc2CpgFixture(withOssDataflow = true) {
 
   "intra-procedural" in {
@@ -473,6 +475,53 @@ class RegexDefinedFlowsDataFlowTests
       snk.reachableByFlows(src).size shouldBe 2
     }
   }
+  "Import statement with method ref sample four" in {
+    val controller =
+      """
+        |from django.contrib import admin
+        |from django.urls import path
+        |from django.conf.urls import url
+        |from .views import all_page
+        |
+        |urlpatterns = [
+        |    url(r'allPage', all_page)
+        |]
+        |""".stripMargin
+    val views =
+      """
+        |def all_page(request):
+        |	print("All pages")
+        |""".stripMargin
+    val cpg = code(controller, Seq("controller", "urls.py").mkString(File.separator))
+      .moreCode(views, Seq("controller", "views.py").mkString(File.separator))
+
+    val args = cpg.call.methodFullName("django.*[.](path|url)").l.head.argument.l
+    args.size shouldBe 3
+  }
+
+  "Import statement with method ref sample five" in {
+    val controller =
+      """
+        |from django.contrib import admin
+        |from django.urls import path
+        |from django.conf.urls import url
+        |from student.views import all_page
+        |
+        |urlpatterns = [
+        |    url(r'allPage', all_page)
+        |]
+        |""".stripMargin
+    val views =
+      """
+        |def all_page(request):
+        |	print("All pages")
+        |""".stripMargin
+    val cpg = code(controller, Seq("controller", "urls.py").mkString(File.separator))
+      .moreCode(views, Seq("student", "views.py").mkString(File.separator))
+
+    val args = cpg.call.methodFullName("django.*[.](path|url)").l.head.argument.l
+    args.size shouldBe 3
+  }
 
   "flows via tuple literal" should {
     val cpg = code("""
diff --git a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/passes/TypeRecoveryPassTests.scala b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/passes/TypeRecoveryPassTests.scala
index 32b82e2db..290ee9d77 100644
--- a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/passes/TypeRecoveryPassTests.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/passes/TypeRecoveryPassTests.scala
@@ -1,11 +1,11 @@
 package io.joern.pysrc2cpg.passes
 
 import io.joern.pysrc2cpg.PySrc2CpgFixture
-import io.joern.x2cpg.passes.frontend.XTypeHintCallLinker
+import io.joern.x2cpg.passes.frontend.{ImportsPass, XTypeHintCallLinker}
 import io.shiftleft.semanticcpg.language._
 
 import java.io.File
-
+import io.joern.x2cpg.passes.frontend.ImportsPass._
 class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
 
   "literals declared from built-in types" should {
@@ -63,6 +63,19 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
         |response = sg.send(message)
         |""".stripMargin).cpg
 
+    "resolve correct imports via tag nodes" in {
+      val List(
+        webClientM: UnknownMethod,
+        webClientT: UnknownTypeDecl,
+        sendGridM: UnknownMethod,
+        sendGridT: UnknownTypeDecl
+      ) = cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      webClientM.fullName shouldBe "slack_sdk.py:<module>.WebClient.__init__"
+      webClientT.fullName shouldBe "slack_sdk.py:<module>.WebClient"
+      sendGridM.fullName shouldBe "sendgrid.py:<module>.SendGridAPIClient.__init__"
+      sendGridT.fullName shouldBe "sendgrid.py:<module>.SendGridAPIClient"
+    }
+
     "resolve 'sg' identifier types from import information" in {
       val List(sgAssignment, sgElseWhere) = cpg.identifier("sg").take(2).l
       sgAssignment.typeFullName shouldBe "sendgrid.py:<module>.SendGridAPIClient"
@@ -200,6 +213,17 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
       "bar.py"
     ).cpg
 
+    "resolve correct imports via tag nodes" in {
+      val List(foo1: UnknownMethod, foo2: UnknownTypeDecl) =
+        cpg.file(".*foo.py").ast.isCall.where(_.referencedImports).tag.toResolvedImport.toList
+      foo1.fullName shouldBe "flask_sqlalchemy.py:<module>.SQLAlchemy.__init__"
+      foo2.fullName shouldBe "flask_sqlalchemy.py:<module>.SQLAlchemy"
+      val List(bar1: ResolvedTypeDecl, bar2: ResolvedMethod) =
+        cpg.file(".*bar.py").ast.isCall.where(_.referencedImports).tag.toResolvedImport.toList
+      bar1.fullName shouldBe "foo.py:<module>"
+      bar2.fullName shouldBe "foo.py:<module>"
+    }
+
     "resolve 'x' and 'y' locally under foo.py" in {
       val Some(x) = cpg.file.name(".*foo.*").ast.isIdentifier.name("x").headOption
       x.typeFullName shouldBe "__builtin.int"
@@ -282,6 +306,21 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
       "app.py"
     ).cpg
 
+    "resolve correct imports via tag nodes" in {
+      val List(a: ResolvedTypeDecl, b: ResolvedMethod, c: UnknownImport, d: ResolvedMember) =
+        cpg.file(".*UserController.py").ast.isCall.where(_.referencedImports).tag.toResolvedImport.toList
+      a.fullName shouldBe "app.py:<module>"
+      b.fullName shouldBe "app.py:<module>"
+      c.path shouldBe "flask.py:<module>.jsonify"
+      d.basePath shouldBe "app.py:<module>"
+      d.memberName shouldBe "db"
+
+      val List(sqlAlchemyM: UnknownMethod, sqlAlchemyT: UnknownTypeDecl) =
+        cpg.file(".*app.py").ast.isCall.where(_.referencedImports).tag.toResolvedImport.toList
+      sqlAlchemyM.fullName shouldBe "flask_sqlalchemy.py:<module>.SQLAlchemy.__init__"
+      sqlAlchemyT.fullName shouldBe "flask_sqlalchemy.py:<module>.SQLAlchemy"
+    }
+
     "be determined as a variable reference and have its type recovered correctly" in {
       cpg.identifier("db").map(_.typeFullName).toSet shouldBe Set("flask_sqlalchemy.py:<module>.SQLAlchemy")
 
@@ -314,6 +353,11 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
         |log.error("foo")
         |""".stripMargin).cpg
 
+    "resolve correct imports via tag nodes" in {
+      val List(logging: UnknownImport) = cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      logging.path shouldBe "logging.py:<module>"
+    }
+
     "provide a dummy type" in {
       val Some(log) = cpg.identifier("log").headOption
       log.typeFullName shouldBe "logging.py:<module>.getLogger.<returnValue>"
@@ -332,6 +376,13 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
         |req = urllib.request.Request(url=apiUrl, data=dataBytes, method='POST')
         |""".stripMargin).cpg
 
+    "resolve correct imports via tag nodes" in {
+      val List(error: UnknownImport, request: UnknownImport) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      error.path shouldBe "urllib.py:<module>.error"
+      request.path shouldBe "urllib.py:<module>.request"
+    }
+
     "reasonably determine the constructor type" in {
       val Some(tmp0) = cpg.identifier("tmp0").headOption
       tmp0.typeFullName shouldBe "urllib.py:<module>.request"
@@ -385,6 +436,17 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
       "pymongo.py"
     ).cpg
 
+    "resolve correct imports via tag nodes" in {
+      val List(a: ResolvedTypeDecl, b: ResolvedMethod, c: UnknownMethod, d: UnknownTypeDecl, e: UnknownImport) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+
+      a.fullName shouldBe "MongoConnection.py:<module>.MongoConnection"
+      b.fullName shouldBe "MongoConnection.py:<module>.MongoConnection.__init__"
+      c.fullName shouldBe "pymongo.py:<module>.MongoClient.__init__"
+      d.fullName shouldBe "pymongo.py:<module>.MongoClient"
+      e.path shouldBe Seq("django", "conf.py:<module>.settings").mkString(File.separator)
+    }
+
     "recover a potential type for `self.collection` using the assignment at `get_collection` as a type hint" in {
       val Some(selfFindFound) = cpg.typeDecl(".*InstallationsDAO.*").ast.isCall.name("find_one").headOption
       selfFindFound.dynamicTypeHintFullName shouldBe Seq(
@@ -496,9 +558,22 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
           |   global __factory
           |   return __factory()
           |""".stripMargin,
-        "data/db_session.py"
+        Seq("data", "db_session.py").mkString(File.separator)
       )
 
+    "resolve correct imports via tag nodes" in {
+      val List(
+        sessionT: ResolvedTypeDecl,
+        sessionM: ResolvedMethod,
+        sqlSessionM: UnknownMethod,
+        sqlSessionT: UnknownTypeDecl
+      ) = cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      sessionT.fullName shouldBe Seq("data", "db_session.py:<module>").mkString(File.separator)
+      sessionM.fullName shouldBe Seq("data", "db_session.py:<module>").mkString(File.separator)
+      sqlSessionM.fullName shouldBe Seq("sqlalchemy", "orm.py:<module>.Session.__init__").mkString(File.separator)
+      sqlSessionT.fullName shouldBe Seq("sqlalchemy", "orm.py:<module>.Session").mkString(File.separator)
+    }
+
     "recover its full name successfully" in {
       val List(methodFullName) = cpg.call("query").methodFullName.l
       methodFullName shouldBe Seq("sqlalchemy", "orm.py:<module>.Session.query").mkString(File.separator)
@@ -527,7 +602,7 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
         |def add_student():
         | pass
         |""".stripMargin,
-      s"student${File.separator}views.py"
+      Seq("student", "views.py").mkString(File.separator)
     )
 
     "recover the method full name related" in {
@@ -577,6 +652,15 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
       Seq("api", "__init__.py").mkString(File.separator)
     )
 
+    "resolve correct imports via tag nodes" in {
+      val List(sqlSessionM: UnknownMethod, sqlSessionT: UnknownTypeDecl, db: ResolvedMember) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      sqlSessionM.fullName shouldBe Seq("flask_sqlalchemy.py:<module>.SQLAlchemy.__init__").mkString(File.separator)
+      sqlSessionT.fullName shouldBe Seq("flask_sqlalchemy.py:<module>.SQLAlchemy").mkString(File.separator)
+      db.basePath shouldBe Seq("api", "__init__.py:<module>").mkString(File.separator)
+      db.memberName shouldBe "db"
+    }
+
     "recover a call to `add`" in {
       val Some(addCall) = cpg.call("add").headOption
       addCall.methodFullName shouldBe "flask_sqlalchemy.py:<module>.SQLAlchemy.<member>(session).add"
@@ -849,10 +933,19 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
       "models.py"
     )
 
+    "resolve correct imports via tag nodes" in {
+      val List(djangoModels: UnknownImport, profileT: ResolvedTypeDecl, profileM: ResolvedMethod) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      djangoModels.path shouldBe Seq("django", "db.py:<module>.models").mkString(File.separator)
+      profileT.fullName shouldBe "models.py:<module>.Profile"
+      profileM.fullName shouldBe "models.py:<module>.Profile.__init__"
+    }
+
     "resolve the `filter` call" in {
       val Some(call) = cpg.call.nameExact("filter").headOption
       call.methodFullName shouldBe "models.py:<module>.Profile.<member>(objects).filter"
     }
+
   }
 
   "Recovered values that are returned in methods" should {
@@ -882,6 +975,13 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
       "impl.py"
     )
 
+    "resolve correct imports via tag nodes" in {
+      val List(connectorT: ResolvedTypeDecl, connectorM: ResolvedMethod) =
+        cpg.call.where(_.referencedImports).tag.toResolvedImport.toList
+      connectorT.fullName shouldBe Seq("lib", "connector.py:<module>.Connector").mkString(File.separator)
+      connectorM.fullName shouldBe Seq("lib", "connector.py:<module>.Connector.__init__").mkString(File.separator)
+    }
+
     "be able to use field accesses as type hints" in {
       val Some(c) = cpg.identifier("c").headOption
       c.typeFullName shouldBe Seq("lib", "connector.py:<module>.Connector").mkString(File.separator)