Repository: joernio/joern
Pull request #5470 (2025-05-06 10:55:47)

[c2cpg] Implement scope-local unique names
Just like in https://github.com/joernio/joern/pull/5468

max-leuthaeuser: sptests adaptation is here: https://github.com/ShiftLeftSecurity/codescience/pull/8259

Patch:
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreator.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreator.scala
index d2dc89632..422a84e19 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreator.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreator.scala
@@ -48,7 +48,7 @@ class AstCreator(
   // where the respective nodes are defined. Instead, we put them under the parent TYPE_DECL in which they are defined.
   // To achieve this we need this extra stack.
   protected val methodAstParentStack: Stack[NewNode] = new Stack()
-  protected val typeRefIdStack                       = new Stack[NewTypeRef]
+  protected val typeRefIdStack: Stack[NewTypeRef]    = new Stack()
 
   def createAst(): DiffGraphBuilder = {
     val fileContent = if (!config.disableFileContent) Option(cdtAst.getRawSignature) else None
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreatorHelper.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreatorHelper.scala
index 5168b9d7f..64beda3c3 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreatorHelper.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreatorHelper.scala
@@ -5,7 +5,6 @@ import io.joern.x2cpg.Ast
 import io.joern.x2cpg.AstNodeBuilder
 import io.joern.x2cpg.AstNodeBuilder.dependencyNode
 import io.joern.x2cpg.SourceFiles
-import io.joern.x2cpg.utils.IntervalKeyPool
 import io.shiftleft.codepropertygraph.generated.nodes.ExpressionNew
 import io.shiftleft.codepropertygraph.generated.nodes.NewCall
 import io.shiftleft.codepropertygraph.generated.nodes.NewNode
@@ -19,13 +18,14 @@ import org.eclipse.cdt.internal.core.dom.parser.c.CASTArrayRangeDesignator
 import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPASTArrayRangeDesignator
 import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPEvaluation
 
+import scala.collection.mutable
 import scala.util.Success
 import scala.util.Try
 
 trait AstCreatorHelper { this: AstCreator =>
 
-  private val fileLocalNameKeyPool         = new IntervalKeyPool(first = 0, last = Long.MaxValue)
-  private val file2OffsetTable: Array[Int] = genFileOffsetTable()
+  private val scopeLocalUniqueNames: mutable.Map[String, Int] = mutable.HashMap.empty
+  private val file2OffsetTable: Array[Int]                    = genFileOffsetTable()
 
   // We use our own call ast creation function since the version in x2cpg treats
   // base as receiver if no receiver is given which does not fit the needs of this
@@ -63,12 +63,20 @@ trait AstCreatorHelper { this: AstCreator =>
     ast
   }
 
-  protected def fileLocalUniqueName(name: String, fullName: String, targetName: String = ""): (String, String) = {
+  protected def scopeLocalUniqueName(targetName: String, fullName: String = ""): String = {
+    val name = if (targetName.isEmpty) { "<anonymous>" }
+    else { s"<$targetName>" }
+    val scopePath = if (fullName.isEmpty) { scope.computeScopePath }
+    else { fullName.stripSuffix(".") }
+    val key = s"$scopePath:$name"
+    val idx = scopeLocalUniqueNames.getOrElseUpdate(key, 0)
+    scopeLocalUniqueNames.update(key, idx + 1)
+    s"$name$idx"
+  }
+
+  protected def scopeLocalUniqueName(name: String, fullName: String, targetName: String): (String, String) = {
     if (name.isEmpty && (fullName.isEmpty || fullName.endsWith("."))) {
-      val newName = targetName match {
-        case ""    => s"<anonymous>${fileLocalNameKeyPool.next}"
-        case other => s"<$other>${fileLocalNameKeyPool.next}"
-      }
+      val newName           = scopeLocalUniqueName(targetName, fullName)
       val resultingFullName = s"$fullName$newName"
       (newName, resultingFullName)
     } else {
@@ -89,18 +97,6 @@ trait AstCreatorHelper { this: AstCreator =>
     offset - lineStartOffset + 1
   }
 
-  private def genFileOffsetTable(): Array[Int] = {
-    cdtAst.getRawSignature.toCharArray.zipWithIndex.collect { case ('\n', idx) => idx + 1 }
-  }
-
-  protected def fileName(node: IASTNode): String = {
-    val path = Try(node.getContainingFilename) match {
-      case Success(value) if value.nonEmpty => value
-      case _                                => filename
-    }
-    SourceFiles.toRelativePath(path, config.inputPath)
-  }
-
   protected def registerType(typeName: String): String = {
     val fixedTypeName = replaceQualifiedNameSeparator(typeName)
     global.usedTypes.putIfAbsent(fixedTypeName, true)
@@ -127,11 +123,6 @@ trait AstCreatorHelper { this: AstCreator =>
     }
   }
 
-  protected def safeGetBinding(name: IASTName): Option[IBinding] = {
-    // In case of unresolved includes etc. this may fail throwing an unrecoverable exception
-    Try(name.resolveBinding()).toOption.filter(_ != null)
-  }
-
   protected def safeGetBinding(spec: IASTNamedTypeSpecifier): Option[IBinding] = {
     // In case of unresolved includes etc. this may fail throwing an unrecoverable exception
     safeGetBinding(spec.getName).collect {
@@ -139,6 +130,11 @@ trait AstCreatorHelper { this: AstCreator =>
     }
   }
 
+  protected def safeGetBinding(name: IASTName): Option[IBinding] = {
+    // In case of unresolved includes etc. this may fail throwing an unrecoverable exception
+    Try(name.resolveBinding()).toOption.filter(_ != null)
+  }
+
   protected def notHandledYet(node: IASTNode): Ast = {
     if (!node.isInstanceOf[IASTProblem] && !node.isInstanceOf[IASTProblemHolder]) {
       val text = notHandledText(node)
@@ -186,8 +182,6 @@ trait AstCreatorHelper { this: AstCreator =>
     }
   }
 
-  protected def isIncludedNode(node: IASTNode): Boolean = fileName(node) != filename
-
   protected def astsForComments(iASTTranslationUnit: IASTTranslationUnit): Seq[Ast] = {
     if (config.includeComments) {
       iASTTranslationUnit.getComments.toList.filterNot(isIncludedNode).map(comment => astForComment(comment))
@@ -196,6 +190,16 @@ trait AstCreatorHelper { this: AstCreator =>
     }
   }
 
+  protected def isIncludedNode(node: IASTNode): Boolean = fileName(node) != filename
+
+  protected def fileName(node: IASTNode): String = {
+    val path = Try(node.getContainingFilename) match {
+      case Success(value) if value.nonEmpty => value
+      case _                                => filename
+    }
+    SourceFiles.toRelativePath(path, config.inputPath)
+  }
+
   protected def astForNode(node: IASTNode): Ast = {
     node match {
       case expr: IASTExpression             => astForExpression(expr)
@@ -218,6 +222,10 @@ trait AstCreatorHelper { this: AstCreator =>
     }
   }
 
+  private def genFileOffsetTable(): Array[Int] = {
+    cdtAst.getRawSignature.toCharArray.zipWithIndex.collect { case ('\n', idx) => idx + 1 }
+  }
+
   private def notHandledText(node: IASTNode): String = {
     s"""Node '${node.getClass.getSimpleName}' not handled yet!
        |  Code: '${shortenCode(node.getRawSignature)}'
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForFunctionsCreator.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForFunctionsCreator.scala
index 7edf37483..c6cf95a5a 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForFunctionsCreator.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForFunctionsCreator.scala
@@ -318,7 +318,7 @@ trait AstForFunctionsCreator { this: AstCreator =>
         (
           s.getDeclarators.headOption
             .map(n => ASTStringUtil.getSimpleName(n.getName))
-            .getOrElse(fileLocalUniqueName("", "", "param")._1),
+            .getOrElse(scopeLocalUniqueName("param")),
           code(s),
           typeForDeclSpecifier(s),
           false
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForPrimitivesCreator.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForPrimitivesCreator.scala
index 6cd1454af..6646dfcab 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForPrimitivesCreator.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForPrimitivesCreator.scala
@@ -137,7 +137,7 @@ trait AstForPrimitivesCreator { this: AstCreator =>
       case id: IASTIdExpression            => shortName(id)
       case id: IASTName =>
         val name = stripTemplateTags(ASTStringUtil.getSimpleName(id))
-        if (name.isEmpty) safeGetBinding(id).map(_.getName).getOrElse(fileLocalUniqueName("", "")._1)
+        if (name.isEmpty) safeGetBinding(id).map(_.getName).getOrElse(scopeLocalUniqueName(""))
         else name
       case _ => code(ident)
     }
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForStatementsCreator.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForStatementsCreator.scala
index de4a34ef3..cb9972726 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForStatementsCreator.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForStatementsCreator.scala
@@ -94,7 +94,7 @@ trait AstForStatementsCreator { this: AstCreator =>
     }
 
     val initializer  = init.getOrElse(struct.getInitializer)
-    val tmpName      = fileLocalUniqueName("", "", "tmp")._1
+    val tmpName      = scopeLocalUniqueName("tmp")
     val tpe          = registerType(typeFor(initializer))
     val localTmpNode = localNode(struct, tmpName, tmpName, tpe)
     scope.addVariable(tmpName, localTmpNode, tpe, VariableScopeManager.ScopeType.BlockScope)
@@ -377,7 +377,7 @@ trait AstForStatementsCreator { this: AstCreator =>
     val idType         = registerType(typeFor(forStmt.getDeclaration))
 
     // iterator assignment:
-    val iteratorName      = fileLocalUniqueName("", "", "iterator")._1
+    val iteratorName      = scopeLocalUniqueName("iterator")
     val iteratorLocalNode = localNode(forStmt, iteratorName, iteratorName, registerType(Defines.Iterator)).order(0)
     val iteratorNode      = identifierNode(forStmt, iteratorName, iteratorName, Defines.Iterator)
     diffGraph.addEdge(blockNode, iteratorLocalNode, EdgeTypes.AST)
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForTypesCreator.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForTypesCreator.scala
index d8f5a9bb6..79df49cad 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForTypesCreator.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstForTypesCreator.scala
@@ -56,9 +56,8 @@ trait AstForTypesCreator { this: AstCreator =>
         val typeDefName = if (name.isEmpty) { safeGetBinding(declarator.getName).map(_.getName).getOrElse("") }
         else { name }
         val tpe                = registerType(typeFor(declarator))
-        val (name_, fullName_) = fileLocalUniqueName(cleanType(typeDefName), fullName(declarator))
-        val fixedFullName      = if (fullName_.isEmpty) name else s"$fullName_.$name"
-        Ast(typeDeclNode(declarator, name_, registerType(fixedFullName), filename, code(d), alias = Option(tpe)))
+        val (name_, fullName_) = scopeLocalUniqueName(cleanType(typeDefName), fullName(declarator), "")
+        Ast(typeDeclNode(declarator, name_, registerType(fullName_), filename, code(d), alias = Option(tpe)))
       case d if parentIsClassDef(d) =>
         val tpe = typeForIASTDeclarator(declaration, declarator, index)
         Ast(memberNode(declarator, name, code(declarator), tpe))
@@ -123,14 +122,13 @@ trait AstForTypesCreator { this: AstCreator =>
   }
 
   protected def astForAliasDeclaration(aliasDeclaration: ICPPASTAliasDeclaration): Ast = {
-    val (name, fullName_) = fileLocalUniqueName(aliasDeclaration.getAlias.toString, fullName(aliasDeclaration))
-    val fixedFullName     = if (fullName_.isEmpty) name else s"$fullName_.$name"
+    val (name, fullName_) = scopeLocalUniqueName(aliasDeclaration.getAlias.toString, fullName(aliasDeclaration), "")
     val mappedName        = registerType(typeFor(aliasDeclaration.getMappingTypeId))
     val typeDeclNode_ =
       typeDeclNode(
         aliasDeclaration,
         name,
-        registerType(fixedFullName),
+        registerType(fullName_),
         fileName(aliasDeclaration),
         code(aliasDeclaration),
         alias = Option(mappedName)
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/FullNameProvider.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/FullNameProvider.scala
index b46212d77..ebc512ad1 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/FullNameProvider.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/FullNameProvider.scala
@@ -136,6 +136,7 @@ trait FullNameProvider { this: AstCreator =>
         val qualifiedName = node match {
           case _: IASTTranslationUnit                            => ""
           case alias: ICPPASTNamespaceAlias                      => fullNameForICPPASTNamespaceAlias(alias)
+          case aliasDecl: ICPPASTAliasDeclaration                => fullNameForICPPASTAliasDeclaration(aliasDecl)
           case namespace: ICPPASTNamespaceDefinition             => fullNameForICPPASTNamespaceDefinition(namespace)
           case compType: IASTCompositeTypeSpecifier              => fullNameForIASTCompositeTypeSpecifier(compType)
           case enumSpecifier: IASTEnumerationSpecifier           => fullNameForIASTEnumerationSpecifier(enumSpecifier)
@@ -146,6 +147,7 @@ trait FullNameProvider { this: AstCreator =>
           case _: ICPPASTLambdaExpression                        => fullNameForICPPASTLambdaExpression()
           case qfn: ICPPASTQualifiedName                         => fullNameForICPPASTQualifiedName(qfn)
           case id: IASTIdExpression                              => fullNameForIASTIdExpression(id)
+          case decl: IASTDeclarator                              => fullNameForIASTDeclarator(decl)
           case u: IASTUnaryExpression                            => code(u.getOperand)
           case other if other != null && other.getParent != null => fullName(other.getParent)
           case other if other != null                            => notHandledYet(other); ""
@@ -174,6 +176,10 @@ trait FullNameProvider { this: AstCreator =>
     }
   }
 
+  private def fullNameForIASTDeclarator(decl: IASTDeclarator): String = {
+    Try(ASTStringUtil.getQualifiedName(decl.getName)).getOrElse(nextClosureName())
+  }
+
   private def fullNameForIASTIdExpression(id: IASTIdExpression): String = {
     Try(ASTStringUtil.getQualifiedName(id.getName)).getOrElse(nextClosureName())
   }
@@ -186,6 +192,10 @@ trait FullNameProvider { this: AstCreator =>
     ASTStringUtil.getQualifiedName(alias.getMappingName)
   }
 
+  private def fullNameForICPPASTAliasDeclaration(alias: ICPPASTAliasDeclaration): String = {
+    ASTStringUtil.getQualifiedName(alias.getAlias)
+  }
+
   private def fullNameForICPPASTLambdaExpression(): String = {
     val globalFullName = MetaDataPass.getGlobalNamespaceBlockFullName(Some(filename))
     val fullName = scope.computeScopePath match {
@@ -370,6 +380,16 @@ trait FullNameProvider { this: AstCreator =>
           case Some(b: ICPPBinding) if b.getName.nonEmpty => Option(b.getQualifiedName.mkString("."))
           case _                                          => None
         }
+      case e: IASTDeclarator =>
+        safeGetBinding(e.getName) match {
+          case Some(b: ICPPBinding) if b.getName.nonEmpty => Option(b.getQualifiedName.mkString("."))
+          case _                                          => None
+        }
+      case e: ICPPASTAliasDeclaration =>
+        safeGetBinding(e.getAlias) match {
+          case Some(b: ICPPBinding) if b.getName.nonEmpty => Option(b.getQualifiedName.mkString("."))
+          case _                                          => None
+        }
       case _ => None
     }
   }
@@ -388,8 +408,8 @@ trait FullNameProvider { this: AstCreator =>
         case decl: IASTSimpleDeclaration =>
           decl.getDeclarators.headOption
             .map(shortName)
-            .getOrElse(fileLocalUniqueName("", "", "type")._1)
-        case _ => fileLocalUniqueName("", "", "type")._1
+            .getOrElse(scopeLocalUniqueName("type"))
+        case _ => scopeLocalUniqueName("type")
       }
       s"${scope.computeScopePath}.$name"
     }
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/TypeNameProvider.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/TypeNameProvider.scala
index b2b6341b6..9bf032e47 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/TypeNameProvider.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/TypeNameProvider.scala
@@ -141,12 +141,12 @@ trait TypeNameProvider { this: AstCreator =>
       case e: IASTEnumerationSpecifier =>
         val name_                              = shortName(e)
         val fullName_                          = fullName(e)
-        val (uniqueName_, uniqueNameFullName_) = fileLocalUniqueName(name_, fullName_, "enum")
+        val (uniqueName_, uniqueNameFullName_) = scopeLocalUniqueName(name_, fullName_, "enum")
         TypeFullNameInfo(uniqueName_, uniqueNameFullName_)
       case n: ICPPASTNamespaceDefinition =>
         val name_                              = shortName(n)
         val fullName_                          = fullName(n)
-        val (uniqueName_, uniqueNameFullName_) = fileLocalUniqueName(name_, fullName_, "namespace")
+        val (uniqueName_, uniqueNameFullName_) = scopeLocalUniqueName(name_, fullName_, "namespace")
         TypeFullNameInfo(uniqueName_, uniqueNameFullName_)
       case a: ICPPASTNamespaceAlias =>
         val name_     = shortName(a)


Test patch:
diff --git a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/NamespaceTypeTests.scala b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/NamespaceTypeTests.scala
index 2dc277bf0..133edab96 100644
--- a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/NamespaceTypeTests.scala
+++ b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/NamespaceTypeTests.scala
@@ -121,7 +121,7 @@ class NamespaceTypeTests extends C2CpgSuite(fileSuffix = FileDefaults.CppExt) {
       inside(cpg.namespaceBlock.nameNot("<global>").sortBy(_.fullName).l) { case List(unnamed1, namespaceA, unnamed2) =>
         unnamed1.fullName shouldBe "<namespace>0"
         namespaceA.fullName shouldBe "A"
-        unnamed2.fullName shouldBe "A.<namespace>1"
+        unnamed2.fullName shouldBe "A.<namespace>0"
       }
 
       inside(cpg.method.internal.nameNot("<global>").fullName.l) { case List(f, g, h) =>