diff --git a/joern-cli/frontends/javasrc2cpg/build.sbt b/joern-cli/frontends/javasrc2cpg/build.sbt index 130b79c3c0cc..f8c33d985049 100644 --- a/joern-cli/frontends/javasrc2cpg/build.sbt +++ b/joern-cli/frontends/javasrc2cpg/build.sbt @@ -10,7 +10,8 @@ libraryDependencies ++= Seq( "org.projectlombok" % "lombok" % Versions.lombok, "org.scala-lang.modules" %% "scala-parallel-collections" % Versions.scalaParallel, "org.scala-lang.modules" %% "scala-parser-combinators" % Versions.scalaParserCombinators, - "net.lingala.zip4j" % "zip4j" % Versions.zip4j + "net.lingala.zip4j" % "zip4j" % Versions.zip4j, + "org.ow2.asm" % "asm" % Versions.asm, ) enablePlugins(JavaAppPackaging, LauncherJarPlugin) diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/AstCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/AstCreator.scala index 3695e1ff46b6..4230465d48ed 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/AstCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/AstCreator.scala @@ -27,7 +27,7 @@ import com.github.javaparser.resolution.declarations.{ import com.github.javaparser.resolution.types.ResolvedType import com.github.javaparser.resolution.types.parametrization.ResolvedTypeParametersMap import com.github.javaparser.symbolsolver.JavaSymbolSolver -import io.joern.javasrc2cpg.astcreation.declarations.AstForDeclarationsCreator +import io.joern.javasrc2cpg.astcreation.declarations.{AstForDeclarationsCreator, BinarySignatureCalculator} import io.joern.javasrc2cpg.astcreation.expressions.AstForExpressionsCreator import io.joern.javasrc2cpg.astcreation.statements.AstForStatementsCreator import io.joern.javasrc2cpg.scope.Scope @@ -107,6 +107,7 @@ class AstCreator( private[astcreation] val typeInfoCalc: TypeInfoCalculator = TypeInfoCalculator(global, symbolSolver, keepTypeArguments) private[astcreation] val bindingTableCache = mutable.HashMap.empty[String, BindingTable] + private[astcreation] val binarySignatureCalculator: BinarySignatureCalculator = new BinarySignatureCalculator(scope) private[astcreation] val tempNameProvider: TemporaryNameProvider = new TemporaryNameProvider diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForMethodsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForMethodsCreator.scala index 79d79b8f5f06..596327cf7978 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForMethodsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForMethodsCreator.scala @@ -82,7 +82,10 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => ExpectedType(returnTypeFullName, expectedReturnType), methodDeclaration.isStatic() ) - typeParameters.foreach { typeParameter => scope.addTopLevelType(typeParameter.name, typeParameter.typeFullName) } + typeParameters.foreach { typeParameter => scope.addTypeParameter(typeParameter.name, typeParameter.typeFullName) } + + val genericSignature = binarySignatureCalculator.methodBinarySignature(methodDeclaration) + methodNode.genericSignature(genericSignature) val parameterAsts = astsForParameterList(methodDeclaration.getParameters.asScala.toList) val parameterTypes = argumentTypesForMethodLike(maybeResolved) @@ -101,7 +104,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => val thisAst = thisNode.map(Ast(_)).toList thisNode.foreach { node => - scope.enclosingMethod.get.addParameter(node) + scope.enclosingMethod.get.addParameter(node, scope.enclosingTypeDecl.get.typeDecl.genericSignature) } val bodyAst = methodDeclaration.getBody.toScala @@ -145,6 +148,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => val methodReturn = newMethodReturnNode(parameterTypeFullName, line = line(parameter), column = column(parameter)) + val genericSignature = binarySignatureCalculator.recordParameterAccessorBinarySignature(parameter) val methodRoot = methodNode( parameter, parameterName, @@ -153,7 +157,8 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => Option(signature), filename, Option(NodeTypes.TYPE_DECL), - Option(recordTypeFullName) + Option(recordTypeFullName), + genericSignature = Option(genericSignature) ) val modifier = newModifierNode(ModifierTypes.PUBLIC) @@ -230,7 +235,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => List(accessModifier, abstractModifier, staticVirtualModifier).flatten } - private def getIdentifiersForTypeParameters(methodDeclaration: MethodDeclaration): List[NewIdentifier] = { + private def getIdentifiersForTypeParameters(methodDeclaration: CallableDeclaration[?]): List[NewIdentifier] = { methodDeclaration.getTypeParameters.asScala.map { typeParameter => val name = typeParameter.getNameAsString val typeFullName = tryWithSafeStackOverflow(typeParameter.getTypeBound.asScala.headOption).toOption.flatten @@ -264,13 +269,17 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => } def astForDefaultConstructor(originNode: Node, instanceFieldDeclarations: List[FieldDeclaration]): Ast = { + val parameters = scope.enclosingTypeDecl.get.recordParameters + val genericSignature = binarySignatureCalculator.defaultConstructorSignature(parameters) val constructorNode = NewMethod() .name(io.joern.x2cpg.Defines.ConstructorMethodName) .filename(filename) .isExternal(false) + .genericSignature(genericSignature) + .lineNumber(line(originNode)) + .columnNumber(column(originNode)) scope.pushMethodScope(constructorNode, ExpectedType.Void, isStatic = false) - val parameters = scope.enclosingTypeDecl.get.recordParameters val parameterAsts = parameters.zipWithIndex.map { case (param, idx) => astForParameter(param, idx + 1) } @@ -289,7 +298,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => constructorNode.signature(signature) val thisNode = thisNodeForMethod(typeFullName, lineNumber = None, columnNumber = None) - scope.enclosingMethod.foreach(_.addParameter(thisNode)) + scope.enclosingMethod.foreach(_.addParameter(thisNode, scope.enclosingTypeDecl.get.typeDecl.genericSignature)) val recordParameterAssignments = parameterAsts .flatMap(_.nodes) .collect { case param: nodes.NewMethodParameterIn => param } @@ -392,7 +401,8 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => val annotationAsts = parameter.getAnnotations.asScala.map(astForAnnotationExpr) val ast = Ast(parameterNode) - scope.enclosingMethod.get.addParameter(parameterNode) + scope.enclosingMethod.get + .addParameter(parameterNode, binarySignatureCalculator.variableBinarySignature(parameter.getType)) ast.withChildren(annotationAsts) } @@ -463,15 +473,21 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => instanceFieldDeclarations: List[FieldDeclaration] ): List[PartialConstructorDeclaration] = { constructorDeclarations.map { constructorDeclaration => - val constructorNode = createPartialMethod(constructorDeclaration) - .name(io.joern.x2cpg.Defines.ConstructorMethodName) - - scope.pushMethodScope(constructorNode, ExpectedType.Void, isStatic = false) val maybeResolved = Option .when(constructorDeclaration.isConstructorDeclaration)( tryWithSafeStackOverflow(constructorDeclaration.resolve()).toOption ) .flatten + val constructorNode = createPartialMethod(constructorDeclaration) + .name(io.joern.x2cpg.Defines.ConstructorMethodName) + + scope.pushMethodScope(constructorNode, ExpectedType.Void, isStatic = false) + constructorDeclaration match { + case regularConstructor: ConstructorDeclaration => + val typeParameters = getIdentifiersForTypeParameters(regularConstructor) + typeParameters.foreach(typeParam => scope.addTypeParameter(typeParam.name, typeParam.typeFullName)) + case _ => // Compact constructor cannot have type parameters + } val parameters = constructorDeclaration match { case regularConstructor: ConstructorDeclaration => regularConstructor.getParameters.asScala.toList @@ -497,15 +513,17 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => .fullName(fullName) .signature(signature) - parameterAsts.foreach { ast => + parameterAsts.zip(parameters).foreach { (ast, parameterNode) => ast.root match { - case Some(parameter: NewMethodParameterIn) => scope.enclosingMethod.get.addParameter(parameter) - case _ => // This should never happen + case Some(parameter: NewMethodParameterIn) => + val genericType = binarySignatureCalculator.variableBinarySignature(parameterNode.getType) + scope.enclosingMethod.get.addParameter(parameter, genericType) + case _ => // This should never happen } } val thisNode = thisNodeForMethod(typeFullName, line(constructorDeclaration), column(constructorDeclaration)) - scope.enclosingMethod.get.addParameter(thisNode) + scope.enclosingMethod.get.addParameter(thisNode, scope.enclosingTypeDecl.get.typeDecl.genericSignature) scope.pushBlockScope() val recordParameterAssignments = constructorDeclaration match { @@ -677,7 +695,22 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => val endColumn = declaration.getEnd.map(x => Integer.valueOf(x.column)).toScala val placeholderFullName = "" - methodNode(declaration, declaration.getNameAsString(), methodCode, placeholderFullName, None, filename) + + val genericSignature = declaration match { + case callableDeclaration: CallableDeclaration[_] => + binarySignatureCalculator.methodBinarySignature(callableDeclaration) + case compactConstructor: CompactConstructorDeclaration => + binarySignatureCalculator.defaultConstructorSignature(scope.enclosingTypeDecl.get.recordParameters) + } + methodNode( + declaration, + declaration.getNameAsString(), + methodCode, + placeholderFullName, + None, + filename, + genericSignature = Option(genericSignature) + ) } def thisNodeForMethod( diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForTypeDeclsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForTypeDeclsCreator.scala index cc15b3a09370..1567c51ef8b4 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForTypeDeclsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForTypeDeclsCreator.scala @@ -91,6 +91,10 @@ object AstForTypeDeclsCreator { private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => private val logger = LoggerFactory.getLogger(this.getClass) + private def outerClassGenericSignature: Option[String] = { + scope.enclosingTypeDecl.map(decl => binarySignatureCalculator.variableBinarySignature(decl.typeDecl.name)) + } + def astForAnonymousClassDecl( expr: ObjectCreationExpr, body: List[BodyDeclaration[?]], @@ -100,6 +104,7 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => ): Ast = { val (astParentType, astParentFullName) = getAstParentInfo() + val genericSignature = binarySignatureCalculator.variableBinarySignature(expr.getType) val typeDeclRoot = typeDeclNode( expr, @@ -109,16 +114,23 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => expr.toString(), astParentType, astParentFullName, - baseTypeFullName.getOrElse(TypeConstants.Object) :: Nil + baseTypeFullName.getOrElse(TypeConstants.Object) :: Nil, + genericSignature = Option(genericSignature) ) - typeFullName.foreach(scope.addInnerType(typeName, _)) + typeFullName.foreach(typeFullName => scope.addInnerType(typeName, typeFullName, typeFullName)) val declaredMethodNames = body.collect { case methodDeclaration: MethodDeclaration => methodDeclaration.getNameAsString }.toSet - scope.pushTypeDeclScope(typeDeclRoot, scope.isEnclosingScopeStatic, declaredMethodNames, Nil) + scope.pushTypeDeclScope( + typeDeclRoot, + scope.isEnclosingScopeStatic, + outerClassGenericSignature, + declaredMethodNames, + Nil + ) val memberAsts = astsForTypeDeclMembers(expr, body, isInterface = false, typeFullName) val localDecls = scope.localDeclsInScope @@ -158,11 +170,16 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => val name = localClassDecl.getClassDeclaration.getNameAsString val enclosingMethodPrefix = scope.enclosingMethod.getMethodFullName.takeWhile(_ != ':') val fullName = s"$enclosingMethodPrefix.$name" - scope.addInnerType(name, fullName) - astForTypeDeclaration(localClassDecl.getClassDeclaration, fullNameOverride = Some(fullName)) + scope.addInnerType(name, fullName, fullName) + astForTypeDeclaration(localClassDecl.getClassDeclaration, fullNameOverride = Some(fullName), isLocalClass = true) } - def astForTypeDeclaration(typeDeclaration: TypeDeclaration[?], fullNameOverride: Option[String] = None): Ast = { + def astForTypeDeclaration( + typeDeclaration: TypeDeclaration[?], + fullNameOverride: Option[String] = None, + isLocalClass: Boolean = false + ): Ast = { + val isInterface = typeDeclaration match { case classDeclaration: ClassOrInterfaceDeclaration => classDeclaration.isInterface case _ => false @@ -173,19 +190,34 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => val typeDeclRoot = createTypeDeclNode(typeDeclaration, astParentType, astParentFullName, isInterface, fullNameOverride) + // If this is a nested type (which must be true if an enclosing decl exists at this point), then the internal name + // of the class, e.g. Foo$Bar must be added to the scope to make lookups for type Bar possible. + scope.enclosingTypeDecl.foreach { _ => + if (!isLocalClass) + scope.addInnerType(typeDeclaration.getNameAsString, typeDeclRoot.fullName, typeDeclRoot.name) + } + val declaredMethodNames = typeDeclaration.getMethods.asScala.map(_.getNameAsString).toSet - val (recordParameters, recordParameterAsts) = typeDeclaration match { - case recordDeclaration: RecordDeclaration => - val parameters = recordDeclaration.getParameters.asScala.toList - val asts = astsForRecordParameters(recordDeclaration, typeDeclRoot.fullName) - (parameters, asts) - case _ => (Nil, Nil) + val recordParameters = typeDeclaration match { + case recordDeclaration: RecordDeclaration => recordDeclaration.getParameters.asScala.toList + case _ => Nil } - scope.pushTypeDeclScope(typeDeclRoot, typeDeclaration.isStatic, declaredMethodNames, recordParameters) + scope.pushTypeDeclScope( + typeDeclRoot, + typeDeclaration.isStatic, + outerClassGenericSignature, + declaredMethodNames, + recordParameters + ) addTypeDeclTypeParamsToScope(typeDeclaration) + val recordParameterAsts = typeDeclaration match { + case recordDeclaration: RecordDeclaration => astsForRecordParameters(recordDeclaration, typeDeclRoot.fullName) + case _ => Nil + } + val annotationAsts = typeDeclaration.getAnnotations.asScala.map(astForAnnotationExpr) val modifiers = modifiersForTypeDecl(typeDeclaration, isInterface) val enumEntries = typeDeclaration match { @@ -254,7 +286,14 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => .getOrElse(defaultTypeFallback(typ)) }.toOption.getOrElse(defaultTypeFallback()) - val parameterMember = memberNode(parameter, parameterName, code(parameter), parameterTypeFullName) + val genericSignature = binarySignatureCalculator.variableBinarySignature(parameter.getType) + val parameterMember = memberNode( + parameter, + parameterName, + code(parameter), + parameterTypeFullName, + genericSignature = Option(genericSignature) + ) val privateModifier = newModifierNode(ModifierTypes.PRIVATE) val memberAst = Ast(parameterMember).withChild(Ast(privateModifier)) @@ -328,7 +367,7 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => members.collect { case typeDeclaration: TypeDeclaration[_] => val (name, fullName) = getTypeDeclNameAndFullName(typeDeclaration, fullNameOverride) - scope.addInnerType(name, fullName) + scope.addInnerType(name, fullName, fullName) } val fields = members.collect { case fieldDeclaration: FieldDeclaration => fieldDeclaration } @@ -493,7 +532,13 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => private def membersForCapturedVariables(originNode: Node, captures: List[ScopeVariable]): List[Ast] = { captures.map { variable => - val node = memberNode(originNode, variable.name, variable.name, variable.typeFullName) + val node = memberNode( + originNode, + variable.name, + variable.name, + variable.typeFullName, + genericSignature = Option(variable.genericSignature) + ) Ast(node) } } @@ -642,9 +687,11 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => val name = v.getName.toString // Use type name without generics stripped in code val variableTypeString = tryWithSafeStackOverflow(v.getTypeAsString).getOrElse("") - val node = memberNode(v, name, s"$variableTypeString $name", typeFullName) - val memberAst = Ast(node) - val annotationAsts = annotations.asScala.map(astForAnnotationExpr) + val genericSignature = binarySignatureCalculator.variableBinarySignature(v.getType) + val node = + memberNode(v, name, s"$variableTypeString $name", typeFullName, genericSignature = Option(genericSignature)) + val memberAst = Ast(node) + val annotationAsts = annotations.asScala.map(astForAnnotationExpr) val fieldDeclModifiers = modifiersForFieldDeclaration(fieldDeclaration) @@ -708,7 +755,18 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => val code = codeForTypeDecl(typ, isInterface) - typeDeclNode(typ, name, fullName, filename, code, astParentType, astParentFullName, baseTypeFullNames) + val genericSignature = binarySignatureCalculator.typeDeclBinarySignature(typ) + typeDeclNode( + typ, + name, + fullName, + filename, + code, + astParentType, + astParentFullName, + baseTypeFullNames, + genericSignature = Option(genericSignature) + ) } private def codeForTypeDecl(typ: TypeDeclaration[?], isInterface: Boolean): String = { @@ -748,15 +806,21 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => } private def addTypeDeclTypeParamsToScope(typ: TypeDeclaration[?]): Unit = { - tryWithSafeStackOverflow(typ.resolve()).map(_.getTypeParameters.asScala) match { - case Success(resolvedTypeParams) => - resolvedTypeParams - .map(identifierForResolvedTypeParameter) - .foreach { typeParamIdentifier => - scope.addTopLevelType(typeParamIdentifier.name, typeParamIdentifier.typeFullName) - } - - case _ => // Nothing to do here + val typeParameters = typ match { + case classOrInterfaceDeclaration: ClassOrInterfaceDeclaration => + classOrInterfaceDeclaration.getTypeParameters.asScala + case recordDeclaration: RecordDeclaration => recordDeclaration.getTypeParameters.asScala + case _ => Nil + } + + typeParameters.foreach { case typeParam => + // TODO: Use typeParam.getTypeBound list to calculate this instead to allow better fallback. + val typeFullName = tryWithSafeStackOverflow(typeParam.resolve().asTypeParameter().getUpperBound).toOption + .flatMap(typeInfoCalc.fullName) + .getOrElse(TypeConstants.Object) + typeInfoCalc.registerType(typeFullName) + + scope.addTypeParameter(typeParam.getNameAsString, typeFullName) } } @@ -765,7 +829,14 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => val typeFullName = tryWithSafeStackOverflow(entry.resolve().getType).toOption.flatMap(typeInfoCalc.fullName) - val entryNode = memberNode(entry, entry.getNameAsString, entry.toString, typeFullName.getOrElse("ANY")) + val genericSignature = binarySignatureCalculator.enumEntryBinarySignature(entry) + val entryNode = memberNode( + entry, + entry.getNameAsString, + entry.toString, + typeFullName.getOrElse("ANY"), + genericSignature = Some(genericSignature) + ) val name = s"${typeFullName.getOrElse(Defines.UnresolvedNamespace)}.${Defines.ConstructorMethodName}" diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/BinarySignatureCalculator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/BinarySignatureCalculator.scala new file mode 100644 index 000000000000..34629db57ca6 --- /dev/null +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/BinarySignatureCalculator.scala @@ -0,0 +1,327 @@ +package io.joern.javasrc2cpg.astcreation.declarations + +import com.github.javaparser.ast.`type`.{ + ArrayType, + ClassOrInterfaceType, + PrimitiveType, + Type, + TypeParameter, + UnknownType, + VarType, + VoidType, + WildcardType +} +import com.github.javaparser.ast.body.{ + AnnotationDeclaration, + CallableDeclaration, + ClassOrInterfaceDeclaration, + ConstructorDeclaration, + EnumConstantDeclaration, + EnumDeclaration, + MethodDeclaration, + Parameter, + RecordDeclaration, + TypeDeclaration +} +import com.github.javaparser.ast.expr.{LambdaExpr, TypePatternExpr} +import com.github.javaparser.printer.configuration.DefaultPrinterConfiguration.ConfigOption +import com.github.javaparser.printer.configuration.{DefaultConfigurationOption, DefaultPrinterConfiguration} +import io.joern.javasrc2cpg.astcreation.declarations.BinarySignatureCalculator.{ + BaseTypeMap, + javaEnumName, + javaObjectName, + javaRecordName, + unspecifiedType +} +import io.joern.javasrc2cpg.scope.Scope +import io.joern.javasrc2cpg.scope.Scope.ScopeTypeParam +import io.joern.javasrc2cpg.typesolvers.TypeInfoCalculator +import io.joern.javasrc2cpg.typesolvers.TypeInfoCalculator.TypeConstants +import io.joern.javasrc2cpg.util.Util +import org.objectweb.asm.signature.SignatureWriter +import org.slf4j.LoggerFactory + +import scala.jdk.CollectionConverters.* +import scala.jdk.OptionConverters.RichOptional + +object BinarySignatureCalculator { + private val javaObjectName = "Object" + private val javaEnumName = "Enum" + private val javaRecordName = "Record" + private val unspecifiedType = "__unspecified_type" + + // From https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-4.html#jvms-4.3 + val BaseTypeMap: Map[String, Char] = Seq( + TypeConstants.Byte -> 'B', + TypeConstants.Char -> 'C', + TypeConstants.Double -> 'D', + TypeConstants.Float -> 'F', + TypeConstants.Int -> 'I', + TypeConstants.Long -> 'J', + TypeConstants.Short -> 'S', + TypeConstants.Boolean -> 'Z', + TypeConstants.Void -> 'V' + ).toMap +} + +class BinarySignatureCalculator(scope: Scope) { + + private val logger = LoggerFactory.getLogger(this.getClass) + + private val typePrinterOptions = new DefaultPrinterConfiguration() + .removeOption(new DefaultConfigurationOption(ConfigOption.PRINT_COMMENTS)) + .removeOption(new DefaultConfigurationOption(ConfigOption.PRINT_JAVADOC)) + + private def typeToString(typ: Type): String = { + Util.stripGenericTypes(typ.toString(typePrinterOptions)) + } + + val unspecifiedClassType: String = { + val writer = SignatureWriter() + + writer.visitClassType(unspecifiedType) + writer.visitEnd() + + writer.toString + } + + def defaultConstructorSignature(parameters: List[Parameter]): String = { + val writer = SignatureWriter() + + parameters.foreach { param => + writer.visitParameterType() + addType(writer, param.getType) + } + + writer.visitReturnType() + addType(writer, new VoidType()) + + writer.toString + } + + def recordParameterAccessorBinarySignature(parameter: Parameter): String = { + val writer = SignatureWriter() + + writer.visitReturnType() + addType(writer, parameter.getType) + + writer.toString + } + + def enumEntryBinarySignature(enumEntry: EnumConstantDeclaration): String = { + val writer = SignatureWriter() + + enumEntry.getParentNode.toScala collect { case enumDeclaration: EnumDeclaration => + writer.visitClassType(enumDeclaration.getNameAsString) + writer.visitEnd() + } + + writer.toString + } + + def variableBinarySignature(typ: String): String = { + val writer = SignatureWriter() + + BaseTypeMap.get(typ) match { + case Some(baseType) => + writer.visitBaseType(baseType) + + case None => + writer.visitClassType(typ) + writer.visitEnd() + } + + writer.toString + } + + def typeDeclBinarySignature(typeDeclaration: TypeDeclaration[?]): String = { + typeDeclaration match { + case decl: AnnotationDeclaration => annotationDecBinarySignature(decl) + case decl: RecordDeclaration => recordDeclBinarySignature(decl) + case decl: ClassOrInterfaceDeclaration => classDeclBinarySignature(decl) + case decl: EnumDeclaration => enumDeclBinarySignature(decl) + case decl => + throw new IllegalArgumentException( + s"Attempting to get binary signature for unhandled type declaration $typeDeclaration" + ) + } + } + + def annotationDecBinarySignature(annotationDecl: AnnotationDeclaration): String = { + val writer = SignatureWriter() + + writer.visitClassType(javaObjectName) + writer.visitEnd() + + writer.toString + } + + def enumDeclBinarySignature(enumDecl: EnumDeclaration): String = { + val writer = SignatureWriter() + + writer.visitSuperclass() + writer.visitClassType(javaEnumName) + writer.visitTypeArgument('=') + writer.visitClassType(enumDecl.getNameAsString) + writer.visitEnd() + + enumDecl.getImplementedTypes.asScala.foreach(addType(writer, _)) + writer.visitEnd() + + writer.toString + } + + def patternVariableBinarySignature(typePatternExpr: TypePatternExpr): String = { + val writer = SignatureWriter() + addType(writer, typePatternExpr.getType) + writer.toString + } + + def classDeclBinarySignature( + classDecl: ClassOrInterfaceDeclaration, + classNameOverride: Option[String] = None + ): String = { + val writer = SignatureWriter() + classDecl.getTypeParameters.asScala.foreach(addTypeParam(writer, _)) + + writer.visitSuperclass() + if (classDecl.isInterface) { + writer.visitClassType(javaObjectName) + writer.visitEnd() + classDecl.getExtendedTypes.asScala.foreach(addType(writer, _)) + } else { + if (classDecl.getExtendedTypes.isEmpty) { + writer.visitClassType(javaObjectName) + writer.visitEnd() + } else { + classDecl.getExtendedTypes.asScala.foreach(addType(writer, _)) + } + classDecl.getImplementedTypes.asScala.foreach(addType(writer, _)) + } + + writer.toString + } + + def recordDeclBinarySignature(recordDecl: RecordDeclaration): String = { + val writer = SignatureWriter() + recordDecl.getTypeParameters.asScala.foreach(addTypeParam(writer, _)) + + writer.visitSuperclass() + writer.visitClassType(javaRecordName) + writer.visitEnd() + + recordDecl.getImplementedTypes.asScala.foreach(addType(writer, _)) + + writer.toString + } + + def variableBinarySignature(variableType: Type): String = { + val writer = SignatureWriter() + addType(writer, variableType) + writer.toString + } + + def methodBinarySignature(callableDecl: CallableDeclaration[?]): String = { + val writer = SignatureWriter() + callableDecl.getTypeParameters.asScala.foreach(addTypeParam(writer, _)) + + callableDecl.getParameters.asScala.foreach { param => + writer.visitParameterType() + addType(writer, param.getType) + } + + writer.visitReturnType() + callableDecl match { + case methodDeclaration: MethodDeclaration => addType(writer, methodDeclaration.getType) + case constructorDeclaration: ConstructorDeclaration => + BaseTypeMap.get(TypeConstants.Void).foreach(writer.visitBaseType(_)) + } + + callableDecl.getThrownExceptions.asScala.foreach { exception => + writer.visitExceptionType() + addType(writer, exception) + } + + writer.toString + } + + def lambdaMethodBinarySignature(expr: LambdaExpr): String = { + val writer = SignatureWriter() + + expr.getParameters.asScala.foreach { param => + writer.visitParameterType() + addType(writer, param.getType) + } + + writer.visitReturnType() + writer.visitClassType(unspecifiedType) + writer.visitEnd() + + writer.toString + } + + private def addTypeParam(writer: SignatureWriter, typeParam: TypeParameter): Unit = { + writer.visitFormalTypeParameter(typeParam.getNameAsString()) + writer.visitClassBound() + val typeBoundIt = typeParam.getTypeBound.asScala.iterator + if (typeBoundIt.isEmpty) { + writer.visitClassType(javaObjectName) + writer.visitEnd() + } else { + addType(writer, typeBoundIt.next) + } + typeBoundIt.foreach { typeBound => + writer.visitInterfaceBound() + addType(writer, typeBound) + } + } + + private def isTypeVariable(name: String): Boolean = { + scope.lookupScopeType(name, includeWildcards = false).exists(_.isInstanceOf[ScopeTypeParam]) + } + + private def addType(writer: SignatureWriter, typ: Type): Unit = { + val name = typeToString(typ) + typ match { + case _ if isTypeVariable(name) => + writer.visitTypeVariable(typeToString(typ)) + case classOrInterface: ClassOrInterfaceType => + val internalClassName = scope + .lookupScopeType(name, includeWildcards = false) + .map(_.name) + .getOrElse(name) + writer.visitClassType(internalClassName) + val typeArgs = classOrInterface.getTypeArguments.toScala.map(_.asScala).getOrElse(Nil) + typeArgs.foreach { + case wildcardType: WildcardType => + if (wildcardType.getExtendedType.isPresent) { + writer.visitTypeArgument('+') + addType(writer, wildcardType.getExtendedType.get()) + } else if (wildcardType.getSuperType.isPresent) { + writer.visitTypeArgument('-') + addType(writer, wildcardType.getSuperType.get()) + } else writer.visitTypeArgument('*') + case typeArg => + writer.visitTypeArgument('=') + addType(writer, typeArg) + } + writer.visitEnd() + case arrayType: ArrayType => + writer.visitArrayType() + addType(writer, arrayType.getElementType) + case typeParam: TypeParameter => + writer.visitTypeVariable(typeParam.getNameAsString) + case primitiveType: PrimitiveType => + writer.visitBaseType(primitiveType.getType.toDescriptor.charAt(0)) + case varType: VarType => + writer.visitClassType(unspecifiedType) + writer.visitEnd() + case _: VoidType => + writer.visitBaseType('V') + case _: UnknownType => + writer.visitClassType(unspecifiedType) + writer.visitEnd() + } + } + +} diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForCallExpressionsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForCallExpressionsCreator.scala index d4a8887e371b..9bd15461ff37 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForCallExpressionsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForCallExpressionsCreator.scala @@ -113,7 +113,7 @@ trait AstForCallExpressionsCreator { this: AstCreator => val thisIdentifier = identifierNode(call, NameConstants.This, NameConstants.This, typeFullName) scope.lookupVariable(NameConstants.This) match { - case SimpleVariable(ScopeParameter(thisParam)) => diffGraph.addEdge(thisIdentifier, thisParam, EdgeTypes.REF) + case SimpleVariable(ScopeParameter(thisParam, _)) => diffGraph.addEdge(thisIdentifier, thisParam, EdgeTypes.REF) case _ => // Do nothing. This shouldn't happen for valid code, but could occur in cases where methods could not be resolved } val thisAst = Ast(thisIdentifier) @@ -150,7 +150,9 @@ trait AstForCallExpressionsCreator { this: AstCreator => inlinedAstsForObjectCreationExpr(expr, Ast(assignTarget.copy), expectedType, resetAssignmentTargetType = true) assignTarget.typeFullName(allocAndInitAst.allocAst.rootType.getOrElse(defaultTypeFallback())) - val tmpLocal = localNode(expr, tmpName, tmpName, assignTarget.typeFullName) + val genericSignature = binarySignatureCalculator.variableBinarySignature(expr.getType) + val tmpLocal = + localNode(expr, tmpName, tmpName, assignTarget.typeFullName, genericSignature = Option(genericSignature)) val allocAssignCode = s"$tmpName = ${allocAndInitAst.allocAst.rootCodeOrEmpty}" val allocAssignCall = diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForLambdasCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForLambdasCreator.scala index ea336025a68f..98df127f3e47 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForLambdasCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForLambdasCreator.scala @@ -69,7 +69,7 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => // TODO: lambda method scope can be static if no non-static captures are used scope.pushMethodScope(lambdaMethodNode, expectedLambdaType, isStatic = false) - val lambdaBody = astForLambdaBody(lambdaMethodName, expr.getBody, variablesInScope, returnType) + val lambdaBody = astForLambdaBody(expr, lambdaMethodName, expr.getBody, variablesInScope, returnType) val thisParam = lambdaBody.nodes .collect { case identifier: NewIdentifier => identifier } @@ -140,7 +140,16 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => val signature = lambdaMethodSignature(returnType, parameters) val lambdaFullName = composeMethodFullName(enclosingTypeName, lambdaName, signature) - methodNode(lambdaExpr, lambdaName, "", lambdaFullName, Some(signature), filename) + val genericSignature = binarySignatureCalculator.lambdaMethodBinarySignature(lambdaExpr) + methodNode( + lambdaExpr, + lambdaName, + "", + lambdaFullName, + Some(signature), + filename, + genericSignature = Option(genericSignature) + ) } private def createAndPushLambdaTypeDecl( @@ -159,6 +168,7 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => .fullName(lambdaMethodNode.fullName) .name(lambdaMethodNode.name) .inheritsFromTypeFullName(inheritsFromTypeFullName) + .genericSignature(binarySignatureCalculator.unspecifiedClassType) scope.addLocalDecl(Ast(lambdaTypeDeclNode)) lambdaTypeDeclNode @@ -257,6 +267,7 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => } private def defineCapturedVariables( + lambdaNode: LambdaExpr, lambdaMethodName: String, capturedVariables: Seq[ScopeVariable] ): Seq[(ClosureBindingEntry, NewLocal)] = { @@ -267,7 +278,14 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => val closureBindingNode = newClosureBindingNode(closureBindingId, name, EvaluationStrategies.BY_SHARING) val scopeVariable = variables.head - val capturedLocal = newLocalNode(scopeVariable.name, scopeVariable.typeFullName, Option(closureBindingId)) + val capturedLocal = localNode( + lambdaNode, + scopeVariable.name, + scopeVariable.name, + scopeVariable.typeFullName, + Option(closureBindingId), + Option(scopeVariable.genericSignature) + ) scope.enclosingBlock.foreach(_.addLocal(capturedLocal)) ClosureBindingEntry(scopeVariable, closureBindingNode) -> capturedLocal @@ -276,6 +294,7 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => } private def astForLambdaBody( + lambdaExpr: LambdaExpr, lambdaMethodName: String, body: Statement, variablesInScope: Seq[ScopeVariable], @@ -300,7 +319,7 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => stmts.flatMap(_.nodes).collect { case i: NewIdentifier if outerScopeVariableNames.contains(i.name) => outerScopeVariableNames(i.name) } - val bindingsToLocals = defineCapturedVariables(lambdaMethodName, capturedVariables) + val bindingsToLocals = defineCapturedVariables(lambdaExpr, lambdaMethodName, capturedVariables) val capturedLocalAsts = bindingsToLocals.map(_._2).map(Ast(_)) val closureBindingEntries = bindingsToLocals.map(_._1) val temporaryLocalAsts = scope.enclosingMethod.map(_.getTemporaryLocals).getOrElse(Nil).map(Ast(_)) @@ -377,7 +396,8 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => } parameterNodes.foreach { paramNode => - scope.enclosingMethod.get.addParameter(paramNode) + scope.enclosingMethod.get + .addParameter(paramNode, binarySignatureCalculator.unspecifiedClassType) } parameterNodes.map(Ast(_)) diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForPatternExpressionsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForPatternExpressionsCreator.scala index c325b1bb201b..574e75463108 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForPatternExpressionsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForPatternExpressionsCreator.scala @@ -152,9 +152,15 @@ trait AstForPatternExpressionsCreator { this: AstCreator => ) case _ => - val tmpName = tempNameProvider.next - val tmpType = patternInitAst.rootType.getOrElse(TypeConstants.Object) - val tmpLocal = localNode(rootNode, tmpName, tmpName, tmpType) + val tmpName = tempNameProvider.next + val tmpType = patternInitAst.rootType.getOrElse(TypeConstants.Object) + val tmpLocal = localNode( + rootNode, + tmpName, + tmpName, + tmpType, + genericSignature = Option(binarySignatureCalculator.unspecifiedClassType) + ) val tmpIdentifier = identifierNode(rootNode, tmpName, tmpName, tmpType) val tmpAssignmentNode = @@ -216,8 +222,15 @@ trait AstForPatternExpressionsCreator { this: AstCreator => ) .getOrElse(defaultTypeFallback()) } - val variableTypeCode = tryWithSafeStackOverflow(code(typePatternExpr.getType)).getOrElse(variableType) - val patternLocal = localNode(typePatternExpr, variableName, code(typePatternExpr), variableType) + val variableTypeCode = tryWithSafeStackOverflow(code(typePatternExpr.getType)).getOrElse(variableType) + val genericSignature = binarySignatureCalculator.variableBinarySignature(typePatternExpr.getType) + val patternLocal = localNode( + typePatternExpr, + variableName, + code(typePatternExpr), + variableType, + genericSignature = Option(genericSignature) + ) val patternIdentifier = identifierNode(typePatternExpr, variableName, variableName, variableType) val initializerAst = castAstIfNecessary(typePatternExpr, variableType, patternNode.getAst) diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForVarDeclAndAssignsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForVarDeclAndAssignsCreator.scala index 3ddbcd1c4695..4c293de6a868 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForVarDeclAndAssignsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForVarDeclAndAssignsCreator.scala @@ -138,8 +138,15 @@ trait AstForVarDeclAndAssignsCreator { this: AstCreator => // Use type name with generics for code val localCode = s"${declaratorType.map(_.toString).getOrElse("")} ${variableDeclarator.getNameAsString}" + val genericSignature = binarySignatureCalculator.variableBinarySignature(variableDeclarator.getType) val local = - localNode(originNode, variableDeclarator.getNameAsString, localCode, typeFullName) + localNode( + originNode, + variableDeclarator.getNameAsString, + localCode, + typeFullName, + genericSignature = Option(genericSignature) + ) scope.enclosingBlock.foreach(_.addLocal(local)) diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/statements/AstForForLoopsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/statements/AstForForLoopsCreator.scala index b90fe7896ead..96c54ca4ad1a 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/statements/AstForForLoopsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/statements/AstForForLoopsCreator.scala @@ -231,9 +231,16 @@ trait AstForForLoopsCreator { this: AstCreator => iterableAsts.head } - val iterableName = nextIterableName() - val iterableLocalNode = localNode(iterableExpression, iterableName, iterableName, iterableType.getOrElse("ANY")) - val iterableLocalAst = Ast(iterableLocalNode) + val iterableName = nextIterableName() + val genericSignature = binarySignatureCalculator.unspecifiedClassType + val iterableLocalNode = localNode( + iterableExpression, + iterableName, + iterableName, + iterableType.getOrElse("ANY"), + genericSignature = Option(genericSignature) + ) + val iterableLocalAst = Ast(iterableLocalNode) val iterableAssignNode = newOperatorCallNode(Operators.assignment, code = "", line = lineNo, typeFullName = iterableType) @@ -251,14 +258,16 @@ trait AstForForLoopsCreator { this: AstCreator => } private def nativeForEachIdxLocalNode(lineNo: Option[Int]): NewLocal = { - val idxName = nextIndexName() - val typeFullName = TypeConstants.Int + val idxName = nextIndexName() + val typeFullName = TypeConstants.Int + val genericSignature = binarySignatureCalculator.variableBinarySignature(TypeConstants.Int) val idxLocal = NewLocal() .name(idxName) .typeFullName(typeFullName) .code(idxName) .lineNumber(lineNo) + .genericSignature(genericSignature) scope.enclosingBlock.get.addLocal(idxLocal) idxLocal } @@ -338,7 +347,10 @@ trait AstForForLoopsCreator { this: AstCreator => Some(variable) } + val genericSignature = + maybeVariable.map(variable => binarySignatureCalculator.variableBinarySignature(variable.getType)) val partialLocalNode = NewLocal().lineNumber(lineNo) + genericSignature.foreach(partialLocalNode.genericSignature(_)) maybeVariable match { case Some(variable) => @@ -361,11 +373,13 @@ trait AstForForLoopsCreator { this: AstCreator => private def iteratorLocalForForEach(lineNumber: Option[Int]): NewLocal = { val iteratorLocalName = nextIterableName() + val genericSignature = binarySignatureCalculator.variableBinarySignature(TypeConstants.Iterator) NewLocal() .name(iteratorLocalName) .code(iteratorLocalName) .typeFullName(TypeConstants.Iterator) .lineNumber(lineNumber) + .genericSignature(genericSignature) } private def iteratorAssignAstForForEach( diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/JavaScopeElement.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/JavaScopeElement.scala index dc8f4cc6b92c..30a003445e31 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/JavaScopeElement.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/JavaScopeElement.scala @@ -8,6 +8,7 @@ import io.shiftleft.codepropertygraph.generated.nodes.{NewImport, NewMethod, New import scala.collection.mutable import io.joern.javasrc2cpg.astcreation.ExpectedType +import io.joern.javasrc2cpg.scope.TypeType.{ReferenceTypeType, TypeVariableType} import io.joern.javasrc2cpg.util.MultiBindingTableAdapterForJavaparser.JavaparserBindingDeclType import io.shiftleft.codepropertygraph.generated.nodes.NewMethodParameterIn import io.shiftleft.codepropertygraph.generated.nodes.NewLocal @@ -19,10 +20,16 @@ import io.joern.x2cpg.{Ast, ValidationMode} import java.util import scala.jdk.CollectionConverters.* +enum TypeType: + case ReferenceTypeType, TypeVariableType + trait JavaScopeElement(disableTypeFallback: Boolean) { private val variables = mutable.Map[String, ScopeVariable]() private val types = mutable.Map[String, ScopeType]() private var wildcardImports: WildcardImports = NoWildcard + // TODO: This is almost a duplicate of types, but not quite since this stores type variables and local types + // with original names. See if there's a way to combine them + private val declaredTypeTypes = mutable.Map[String, TypeType]() def isStatic: Boolean @@ -30,6 +37,21 @@ trait JavaScopeElement(disableTypeFallback: Boolean) { variables.put(variable.name, variable) } + def addDeclaredTypeType(typeSimpleName: String, isTypeVariable: Boolean): Unit = { + val typeType = + if (isTypeVariable) + TypeVariableType + else { + ReferenceTypeType + } + + declaredTypeTypes.put(typeSimpleName, typeType) + } + + def getDeclaredTypeType(typeSimpleName: String): Option[TypeType] = { + declaredTypeTypes.get(typeSimpleName) + } + def lookupVariable(name: String): VariableLookupResult = { variables.get(name).map(SimpleVariable(_)).getOrElse(NotInScope) } @@ -47,7 +69,7 @@ trait JavaScopeElement(disableTypeFallback: Boolean) { def getNameWithWildcardPrefix(name: String): Option[ScopeType] = { wildcardImports match { - case SingleWildcard(prefix) => Some(ScopeTopLevelType(s"$prefix.$name")) + case SingleWildcard(prefix) => Some(ScopeTopLevelType(s"$prefix.$name", name)) case _ => None } @@ -117,8 +139,8 @@ object JavaScopeElement { // The insertion order should be preserved to ensure stable results when getting unadded variable asts private var patternVariableIndex = 0 - def addParameter(parameter: NewMethodParameterIn): Unit = { - addVariableToScope(ScopeParameter(parameter)) + def addParameter(parameter: NewMethodParameterIn, genericSignature: String): Unit = { + addVariableToScope(ScopeParameter(parameter, genericSignature)) } def addTemporaryLocal(local: NewLocal): Unit = { @@ -181,6 +203,7 @@ object JavaScopeElement { override val isStatic: Boolean, private[scope] val capturedVariables: Map[String, CapturedVariable], outerClassType: Option[String], + outerClassGenericSignature: Option[String], val declaredMethodNames: Set[String], val recordParameters: List[Parameter] )(implicit disableTypeFallback: Boolean) @@ -224,7 +247,9 @@ object JavaScopeElement { def getUsedCaptures(): List[ScopeVariable] = { val outerScope = outerClassType.map(typ => - ScopeLocal(NewLocal().name(NameConstants.OuterClass).typeFullName(typ).code(NameConstants.OuterClass)) + val localNode = NewLocal().name(NameConstants.OuterClass).typeFullName(typ).code(NameConstants.OuterClass) + outerClassGenericSignature.foreach(localNode.genericSignature(_)) + ScopeLocal(localNode) ) val sortedUsedCaptures = usedCaptureParams.toList.sortBy(_.name) diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/Scope.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/Scope.scala index bb9b531a4267..04a1c9fce729 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/Scope.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/Scope.scala @@ -44,6 +44,7 @@ class Scope(implicit val withSchemaValidation: ValidationMode, val disableTypeFa def pushTypeDeclScope( typeDecl: NewTypeDecl, isStatic: Boolean, + outerClassGenericSignature: Option[String] = None, methodNames: Set[String] = Set.empty, recordParameters: List[Parameter] = Nil ): Unit = { @@ -51,7 +52,8 @@ class Scope(implicit val withSchemaValidation: ValidationMode, val disableTypeFa val outerClassType = scopeStack.takeUntil(_.isInstanceOf[TypeDeclScope]) match { case Nil => None - case (head: TypeDeclScope) :: Nil => Option.unless(isStatic)(head.typeDecl.fullName) + case (head: TypeDeclScope) :: Nil => + Option.unless(isStatic)(head.typeDecl.fullName) case head :: Nil => // make exhaustive match checker happy, but is impossible @@ -59,13 +61,20 @@ class Scope(implicit val withSchemaValidation: ValidationMode, val disableTypeFa case scopes => Option - .unless(isStatic || scopes.init.exists(_.isStatic)) { - scopes.lastOption.collectFirst { case typeDeclScope: TypeDeclScope => typeDeclScope.typeDecl.fullName } - } + .unless(isStatic || scopes.init.exists(_.isStatic))(scopes.lastOption.collectFirst { + case typeDeclScope: TypeDeclScope => typeDeclScope.typeDecl.fullName + }) .flatten } - scopeStack = - new TypeDeclScope(typeDecl, isStatic, captures, outerClassType, methodNames, recordParameters) :: scopeStack + scopeStack = new TypeDeclScope( + typeDecl, + isStatic, + captures, + outerClassType, + outerClassGenericSignature, + methodNames, + recordParameters + ) :: scopeStack } def pushNamespaceScope(namespace: NewNamespaceBlock): Unit = { @@ -89,12 +98,17 @@ class Scope(implicit val withSchemaValidation: ValidationMode, val disableTypeFa } def addTopLevelType(name: String, typeFullName: String): Unit = { - val scopeType = ScopeTopLevelType(typeFullName) + val scopeType = ScopeTopLevelType(typeFullName, name) scopeStack.head.addTypeToScope(name, scopeType) } - def addInnerType(name: String, typeFullName: String): Unit = { - val scopeType = ScopeInnerType(typeFullName) + def addInnerType(name: String, typeFullName: String, internalName: String): Unit = { + val scopeType = ScopeInnerType(typeFullName, internalName) + scopeStack.head.addTypeToScope(name, scopeType) + } + + def addTypeParameter(name: String, typeFullName: String): Unit = { + val scopeType = ScopeTypeParam(typeFullName, name) scopeStack.head.addTypeToScope(name, scopeType) } @@ -311,14 +325,15 @@ object Scope { sealed trait ScopeType { def typeFullName: String + def name: String } /** Used for top-level type declarations and imports that do not have captures to be concerned about or synthetic * names in the cpg */ - final case class ScopeTopLevelType(override val typeFullName: String) extends ScopeType + final case class ScopeTopLevelType(override val typeFullName: String, override val name: String) extends ScopeType - final class ScopeInnerType(override val typeFullName: String) extends ScopeType { + final class ScopeInnerType(override val typeFullName: String, override val name: String) extends ScopeType { private val usedCaptures: mutable.ListBuffer[ScopeVariable] = mutable.ListBuffer() override def equals(other: Any): Boolean = { @@ -332,32 +347,39 @@ object Scope { } object ScopeInnerType { - def apply(typeFullName: String): ScopeInnerType = { - new ScopeInnerType(typeFullName) + def apply(typeFullName: String, name: String): ScopeInnerType = { + new ScopeInnerType(typeFullName, name) } } + final case class ScopeTypeParam(override val typeFullName: String, override val name: String) extends ScopeType + sealed trait ScopeVariable { def node: NewVariableNode def typeFullName: String def name: String + def genericSignature: String } final case class ScopeLocal(override val node: NewLocal) extends ScopeVariable { - val typeFullName: String = node.typeFullName - val name: String = node.name + val typeFullName: String = node.typeFullName + val name: String = node.name + val genericSignature: String = node.genericSignature } - final case class ScopeParameter(override val node: NewMethodParameterIn) extends ScopeVariable { + final case class ScopeParameter(override val node: NewMethodParameterIn, override val genericSignature: String) + extends ScopeVariable { val typeFullName: String = node.typeFullName val name: String = node.name } final case class ScopeMember(override val node: NewMember, isStatic: Boolean) extends ScopeVariable { - val typeFullName: String = node.typeFullName - val name: String = node.name + val typeFullName: String = node.typeFullName + val name: String = node.name + val genericSignature: String = node.genericSignature } final case class ScopePatternVariable(override val node: NewLocal, typePatternExpr: TypePatternExpr) extends ScopeVariable { - val typeFullName: String = node.typeFullName - val name: String = node.name + val typeFullName: String = node.typeFullName + val name: String = node.name + val genericSignature: String = node.genericSignature } sealed trait VariableLookupResult { diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/GenericSignatureTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/GenericSignatureTests.scala new file mode 100644 index 000000000000..52a4ccff727a --- /dev/null +++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/GenericSignatureTests.scala @@ -0,0 +1,1084 @@ +package io.joern.javasrc2cpg.querying + +import io.joern.javasrc2cpg.testfixtures.JavaSrcCode2CpgFixture +import io.shiftleft.semanticcpg.language.* + +/** ### Class type signatures + * - In most cases, only the simple name for the class will be used (so `LString;` will be used instead of + * `Ljava/lang/String`) + * + * - Where a qualified name is used in source, that name is used verbatim in the signature, for example + * `Ljava.util.List` (note the `.` were not substituted for `/`. + * + * - For local classes, the name of the class as it appears in the CPG is used in the signature for instances of that + * class (we don't follow the JVM naming scheme for these), for example + * `Ltestpackage.TestClass.testMethod.LocalClass;` + * + * ### Type parameter bounds From the language specification: + * ``` + * TypeParameter: + * Identifier ClassBound {InterfaceBound} + * + * ClassBound: + * : [ReferenceTypeSignature] + * + * InterfaceBound: + * : ReferenceTypeSignature + * ``` + * If a type parameter only has interface bounds I1, I2, ..., then the signature should contain `` (note + * the empty class bound), but in general we won't know if a type is a class or interface without resolving the it, so + * the signature in the CPG will contain `` instead. + * + * ### Unspecified types Where no type name is specified, the special `L__unspecified_type;` type is used in generic + * signatures. This happens in a few places: + * - For lambda return types and lambda parameters which do not have explicit type annotations + * + * - For lambda type decls + * + * - For locals with a `var` type, for example `var x = 42` + * + * - For synthetic locals created for `foreach` loops, for example in `for (String item : items())`, we create a + * temporary `String[] $iterLocal0 = items()` local which will have an unspecified signature (`item` will still + * have the signature `LString;` as expected) + * + * - For synthetic locals created for the LHS of `instanceof` expressions with pattern matching, for example in + * `foo() instanceof String s`, we create an `Object o = foo()` local (since the type depends on the return type of + * `foo`). + */ +class GenericSignatureTests extends JavaSrcCode2CpgFixture { + + "a simple example with primitive types" should { + val cpg = code(""" + |package test; + | + |class Test { + | char charMember; + | + | public void test(boolean b) { + | int x; + | } + |} + |""".stripMargin) + + "have the correct generic signature for locals" in { + cpg.local.genericSignature.l shouldBe List("I") + } + + "have the correct generic signature for a void method with a boolean arg" in { + cpg.method.name("test").genericSignature.l shouldBe List("(Z)V") + } + + "have the correct generic signature for the parameter" in { + cpg.member.name("charMember").genericSignature.l shouldBe List("C") + } + + "have the correct generic signature for the type decl implicitly extending java.lang.Object" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LObject;") + } + } + + "a method with parameters and a non-void return type" should { + val cpg = code(""" + |package test; + | + |class Test { + | public String test(Test t, Integer i) { + | return null; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.name("test").genericSignature.l shouldBe List("(LTest;LInteger;)LString;") + } + } + + "a method with an unresolved return type" should { + val cpg = code(""" + |package test; + | + |class Test { + | public Foo test(Test t) { + | return null; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.name("test").genericSignature.l shouldBe List("(LTest;)LFoo;") + } + } + + "a method with an unresolved parameter" should { + val cpg = code(""" + |package test; + | + |class Test { + | public void test(Foo f) { + | return null; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.name("test").genericSignature.l shouldBe List("(LFoo;)V") + } + } + + "a class extending another class" should { + val cpg = code(""" + |package foo; + | + |class Foo {} + |""".stripMargin).moreCode(""" + |package test; + | + |import foo.Foo; + | + |class Test extends Foo {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LFoo;") + } + } + + "a class implementing an interface" should { + val cpg = code(""" + |package foo; + | + |interface Foo {} + |""".stripMargin).moreCode(""" + |package test; + | + |import foo.Foo; + | + |class Test implements Foo {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LObject;LFoo;") + } + } + + "a class extending another class and implementing an interface" should { + val cpg = code(""" + |package foo; + | + |class Foo {} + |""".stripMargin) + .moreCode(""" + |package bar; + | + |interface Bar {} + |""".stripMargin) + .moreCode(""" + |package test; + | + |import foo.Foo; + |import bar.Bar; + | + |class Test extends Foo implements Bar {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LFoo;LBar;") + } + } + + "a class implementing multiple interfaces" should { + val cpg = code(""" + |package foo; + | + |interface Foo {} + |""".stripMargin) + .moreCode(""" + |package bar; + | + |interface Bar {} + |""".stripMargin) + .moreCode(""" + |package test; + | + |import foo.Foo; + |import bar.Bar; + | + |class Test implements Foo, Bar {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LObject;LFoo;LBar;") + } + } + + "an interface not extending another interface" should { + val cpg = code(""" + |package foo; + | + |interface Foo {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Foo").genericSignature.l shouldBe List("LObject;") + } + } + + "an interface extending another interface" should { + val cpg = code(""" + |package foo; + | + |interface Foo {} + |""".stripMargin).moreCode(""" + |package bar; + | + |import foo.Foo; + | + |interface Bar extends Foo {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Bar").genericSignature.l shouldBe List("LObject;LFoo;") + } + } + + "an interface extending multiple interfaces" should { + val cpg = code(""" + |package foo; + | + |interface Foo {} + |""".stripMargin) + .moreCode(""" + |package bar; + | + |interface Bar {} + |""".stripMargin) + .moreCode(""" + |package test; + | + |import foo.Foo; + |import bar.Bar; + | + |interface Test extends Foo, Bar {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LObject;LFoo;LBar;") + } + } + + "a class extending an unresolved class" should { + val cpg = code(""" + |package test; + | + |class Test extends Foo {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LFoo;") + } + } + + "a class implementing an unresolved interface" should { + val cpg = code(""" + |package test; + | + |class Test implements Foo {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LObject;LFoo;") + } + } + + "a resolved lambda method" should { + val cpg = code(""" + |package test; + | + |import java.util.function.Consumer; + | + |class Test { + | public Consumer test() { + | return s -> System.out.println(s); + | } + |} + |""".stripMargin) + + "have the correct generic signature for the lambda method" in { + cpg.method.name(".*lambda.*").genericSignature.l shouldBe List("(L__unspecified_type;)L__unspecified_type;") + } + + "have an empty generic signature for the lambda type decl" in { + cpg.typeDecl.name(".*lambda.*").genericSignature.l shouldBe List("L__unspecified_type;") + } + } + + "a lambda method with an explicit type annotation" should { + val cpg = code(""" + |package test; + | + |import java.util.function.Consumer; + | + |class Test { + | public Consumer test() { + | return (String s) -> System.out.println(s); + | } + |} + |""".stripMargin) + + "have the correct generic signature for the lambda method" in { + cpg.method.name(".*lambda.*").genericSignature.l shouldBe List("(LString;)L__unspecified_type;") + } + + "have an empty generic signature for the lambda type decl" in { + cpg.typeDecl.name(".*lambda.*").genericSignature.l shouldBe List("L__unspecified_type;") + } + } + + "an unresolved lambda method" should { + val cpg = code(""" + |package test; + | + |class Test { + | public Consumer test() { + | return s -> System.out.println(s); + | } + |} + |""".stripMargin) + + "have the correct generic signature for the lambda method" in { + cpg.method.name(".*lambda.*").genericSignature.l shouldBe List("(L__unspecified_type;)L__unspecified_type;") + } + + "have an empty generic signature for the lambda type decl" in { + cpg.typeDecl.name(".*lambda.*").genericSignature.l shouldBe List("L__unspecified_type;") + } + } + + "a nested class" should { + val cpg = code(""" + |package test; + | + |class Test { + | class Nested {} + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.nameExact("Test$Nested").genericSignature.l shouldBe List("LObject;") + } + } + + "a local class" should { + val cpg = code(""" + |package test; + | + |class Test { + | public void test() { + | class Local {} + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Local").genericSignature.l shouldBe List("LObject;") + } + } + + "an anonymous class extending a resolved class" should { + val cpg = code(""" + |package foo; + | + |class Foo {} + |""".stripMargin).moreCode(""" + |package test; + | + |import foo.Foo; + | + |class Test { + | public void test() { + | Foo f = new Foo() {}; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.nameExact("Foo$0").genericSignature.l shouldBe List("LFoo;") + } + } + + "an anonymous class extending an unresolved class" should { + val cpg = code(""" + |package test; + | + |class Test { + | public void test() { + | Foo f = new Foo() {}; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.nameExact("Foo$0").genericSignature.l shouldBe List("LFoo;") + } + } + + "an anonymous class extending an unresolved class which can be resolved from imports" should { + val cpg = code(""" + |package test; + | + |import foo.Foo; + | + |class Test { + | public void test() { + | Foo f = new Foo() {}; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.nameExact("Foo$0").genericSignature.l shouldBe List("LFoo;") + } + } + + "a local with an array type" should { + val cpg = code(""" + |package test; + | + |class Test { + | public void test() { + | String[] items; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("items").genericSignature.l shouldBe List("[LString;") + } + } + + "a generic local with a single type argument" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |class Test { + | public void test() { + | List list; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("list").genericSignature.l shouldBe List("LList;") + } + } + + "a generic local with a single wildcard type argument" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |class Test { + | public void test() { + | List list; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("list").genericSignature.l shouldBe List("LList<*>;") + } + } + + "a generic local with a single wildcard type argument with an upper bound" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |class Test { + | public void test() { + | List list; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("list").genericSignature.l shouldBe List("LList<+LString;>;") + } + } + + "a generic local with a single wildcard type argument with a lower bound" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |class Test { + | public void test() { + | List list; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("list").genericSignature.l shouldBe List("LList<-LString;>;") + } + } + + "a generic local with multiple type arguments" should { + val cpg = code(""" + |package test; + | + |import java.util.Map; + | + |class Test { + | public void test() { + | Map map; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("map").genericSignature.l shouldBe List("LMap;") + } + } + + "a generic local with nested type arguments" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + |import java.util.Map; + | + |class Test { + | public void test() { + | Map> map; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("map").genericSignature.l shouldBe List("LMap;>;") + } + } + + "a generic local with a type variable type from the method signature" should { + val cpg = code(""" + |package test; + | + |class Test { + | public void test() { + | T t; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("t").genericSignature.l shouldBe List("TT;") + } + } + + "a generic local with a nested type variable type from the method signature" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |class Test { + | public void test() { + | List list; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("list").genericSignature.l shouldBe List("LList;") + } + } + + "a generic local with a type variable type from the class signature" should { + val cpg = code(""" + |import java.util.List; + | + |public class Main { + | public void main(String[] args) { + | T t; + | } + |} + | + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("t").genericSignature.l shouldBe List("TT;") + } + } + + "a generic local with a type variable type as a bound from the class signature" should { + val cpg = code(""" + |import java.util.List; + | + |public class Main { + | public void main(String[] args) { + | List t; + | } + |} + | + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("t").genericSignature.l shouldBe List("LList<+TT;>;") + } + } + + "a method with a generic return type and generic parameters" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |class Test { + | public S test(T t) {} + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.name("test").genericSignature.l shouldBe List("(TT;)TS;") + } + } + + "a type parameter with multiple interface bounds" should { + val cpg = code(""" + |package test; + | + |interface I1 {} + |interface I2 {} + | + |class Test { + | public void test(T t) {} + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.name("test").genericSignature.l shouldBe List("(TT;)V") + } + } + + "a generic member" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |class Test { + | public List list; + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.member.name("list").genericSignature.l shouldBe List("LList;") + } + } + + "an enum typeDecl" should { + val cpg = code(""" + |package test; + | + |enum Test { + | TEST + |} + |""".stripMargin) + + "have the correct generic signature for the type decl" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LEnum;") + } + + "have the correct generic signature for the enum constant" in { + cpg.member.name("TEST").genericSignature.l shouldBe List("LTest;") + } + } + + "a record typeDecl" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |record Test(String value, List list) {} + |""".stripMargin) + + "have the correct generic signature for the record" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LRecord;") + } + + "have the correct generic signature for the record parameter fields" in { + cpg.member.name("value").genericSignature.l shouldBe List("LString;") + cpg.member.name("list").genericSignature.l shouldBe List("LList;") + } + + "have the correct generic signature for the default constructor" in { + cpg.method.nameExact("").genericSignature.l shouldBe List("(LString;LList;)V") + } + + "have the correct generic signature for the record paramater accessors" in { + cpg.method.name("value").genericSignature.l shouldBe List("()LString;") + cpg.method.name("list").genericSignature.l shouldBe List("()LList;") + } + } + + "a type decl extending a generic type" should { + val cpg = code(""" + |package bar; + | + |class Bar {} + |""".stripMargin).moreCode(""" + |package test; + | + |class Test extends Bar {} + |""".stripMargin) + + "have the correct generic signature for the generic class" in { + cpg.typeDecl.name("Bar").genericSignature.l shouldBe List("LObject;") + } + + "have the correct generic signature for the inheriting class" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LBar;") + } + } + + "the lowering for a native foreach loop with a synthetic iterator local" should { + val cpg = code(""" + |package test; + | + |class Test { + | String[] items() { return null; } + | void test() { + | for (String item : items()) {} + | } + |} + |""".stripMargin) + + "have the correct generic signature for the synthetic iterator local" in { + cpg.local.nameExact("$iterLocal0").genericSignature.l shouldBe List("L__unspecified_type;") + } + + "have the correct generic signature for the synthetic index local" in { + cpg.local.nameExact("$idx0").genericSignature.l shouldBe List("I") + } + + "have the correct generic signature for the variable local" in { + cpg.local.name("item").genericSignature.l shouldBe List("LString;") + } + } + + "the lowering for a native foreach loop" should { + val cpg = code(""" + |package test; + | + |class Test { + | void test(String[] items) { + | for (String item : items) {} + | } + |} + |""".stripMargin) + + "have the correct generic signature for the synthetic index local" in { + cpg.local.nameExact("$idx0").genericSignature.l shouldBe List("I") + } + + "have the correct generic signature for the variable local" in { + cpg.local.name("item").genericSignature.l shouldBe List("LString;") + } + } + + "the lowering for an iterator foreach loop" should { + val cpg = code(""" + |package test; + | + |import java.util.List; + | + |class Test { + | void test(List items) { + | for (String item : items) {} + | } + |} + |""".stripMargin) + + "have the correct generic signature for the synthetic iterator local" in { + cpg.local.nameExact("$iterLocal0").genericSignature.l shouldBe List("Ljava.util.Iterator;") + } + + "have the correct generic signature for the variable local" in { + cpg.local.name("item").genericSignature.l shouldBe List("LString;") + } + } + + "the synthetic tmp local in the block representation of a constructor invocation" should { + val cpg = code(""" + |package test; + | + |class Test { + | void test() { + | System.out.println(new String()); + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name.foreach(println) + cpg.local.nameExact("$obj0").genericSignature.l shouldBe List("LString;") + } + } + + "a captured local in a lambda" should { + val cpg = code(""" + |package test; + | + |import java.util.function.Consumer; + | + |class Test { + | public Consumer test(Integer captured) { + | return s -> System.out.println(captured); + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("captured").genericSignature.l shouldBe List("LInteger;") + } + } + + "a pattern initializer requiring a tmp local" should { + val cpg = code(""" + |package test; + | + |class Test { + | public Object foo() { return null; } + | + | public void test() { + | if (foo() instanceof String s) {} + | } + |} + |""".stripMargin) + + "have the correct generic signature for the tmp local" in { + cpg.local.nameExact("$obj0").genericSignature.l shouldBe List("L__unspecified_type;") + } + + "have the correct generic signature for the pattern variable local" in { + cpg.local.name("s").genericSignature.l shouldBe List("LString;") + } + } + + "a local class with captures" should { + val cpg = code(""" + |class Test { + | String mainField; + | + | public void test(Integer testParam) { + | class Foo { + | void foo() { + | System.out.println(mainField + testParam); + | } + | } + | } + |} + |""".stripMargin) + + "have the correct generic signature for the outerClass member" in { + // TODO: This should maybe be `LTest;` instead, but the type variable has no + // meaning in `Foo`. + cpg.member.name("outerClass").genericSignature.l shouldBe List("LTest;") + } + + "have the correct generic signature for a captured parameter member" in { + cpg.member.name("testParam").genericSignature.l shouldBe List("LInteger;") + } + } + + "a class extending a nested class" should { + val cpg = code(""" + |package test; + | + |class Test { + | class Foo {} + | class Bar extends Foo {} + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.nameExact("Test$Bar").genericSignature.l shouldBe List("LTest$Foo;") + } + } + + "a class extending a local class" should { + val cpg = code(""" + |class Test { + | public void test() { + | class Foo {} + | class Bar extends Foo {} + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.nameExact("Bar").genericSignature.l shouldBe List("LTest.test.Foo;") + } + } + + "a default constructor" should { + val cpg = code(""" + |class Test {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.nameExact("").genericSignature.l shouldBe List("()V") + } + } + + "an explicit constructor" should { + val cpg = code(""" + |class Test { + | public Test(String s) {} + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.nameExact("").genericSignature.l shouldBe List("(LString;)V") + } + } + + "a compact constructor for a record" should { + val cpg = code(""" + |record Test(String s) { + | public Test {} + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.nameExact("").genericSignature.l shouldBe List("(LString;)V") + } + } + + "a local with an unresolved fully qualified name" should { + val cpg = code(""" + |class Test { + | public void test() { + | foo.Foo f; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("f").genericSignature.l shouldBe List("Lfoo.Foo;") + } + } + + "a local with an unresolved type which can be inferred from imports" should { + val cpg = code(""" + |import foo.Foo; + | + |class Test { + | public void test() { + | Foo f; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("f").genericSignature.l shouldBe List("LFoo;") + } + } + + "a member with an unresolved fully qualified name" should { + val cpg = code(""" + |class Test { + | foo.Foo f; + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.member.name("f").genericSignature.l shouldBe List("Lfoo.Foo;") + } + } + + "a member with an unresolved type which can be inferred from imports" should { + val cpg = code(""" + |import foo.Foo; + | + |class Test { + | Foo f; + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.member.name("f").genericSignature.l shouldBe List("LFoo;") + } + } + + "a method with an unresolved fully qualified return type and param" should { + val cpg = code(""" + |class Test { + | public foo.Foo test(bar.Bar b) {} + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.name("test").genericSignature.l shouldBe List("(Lbar.Bar;)Lfoo.Foo;") + } + } + + "a method with an unresolved return type and param which can be inferred from imports" should { + val cpg = code(""" + |import foo.Foo; + |import bar.Bar; + | + |class Test { + | public Foo test(Bar b) {} + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.method.name("test").genericSignature.l shouldBe List("(LBar;)LFoo;") + } + } + + "a type decl extending an unresolved fully qualified type" should { + val cpg = code(""" + |class Test extends foo.Foo {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("Lfoo.Foo;") + } + } + + "a type decl extending an unresolved type which can be inferred from imports" should { + val cpg = code(""" + |import foo.Foo; + |import bar.Bar; + | + |class Test extends Foo {} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.typeDecl.name("Test").genericSignature.l shouldBe List("LFoo;") + } + } + + "a local with a var type" should { + val cpg = code(""" + |public class Test { + | public void foo() { + | var s = "hello"; + | } + |} + |""".stripMargin) + + "have the correct generic signature" in { + cpg.local.name("s").genericSignature.l shouldBe List("L__unspecified_type;") + } + } +} diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/util/ScopeTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/util/ScopeTests.scala index fb674818b083..40260e819438 100644 --- a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/util/ScopeTests.scala +++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/util/ScopeTests.scala @@ -19,6 +19,7 @@ import io.joern.x2cpg.ValidationMode class ScopeTests extends AnyFlatSpec with Matchers with BeforeAndAfterAll { private implicit val withSchemaValidation: ValidationMode = ValidationMode.Enabled private implicit val disableTypeFallback: Boolean = false + private val genericSignature = "GENERIC_SIGNATURE" behavior of "javasrc2cpg scope" @@ -28,7 +29,7 @@ class ScopeTests extends AnyFlatSpec with Matchers with BeforeAndAfterAll { val method = NewMethod().name("fooMethod") val scope = new Scope() - scope.pushTypeDeclScope(typeDecl, isStatic = false) + scope.pushTypeDeclScope(typeDecl, isStatic = false, Option(genericSignature)) scope.enclosingTypeDecl.get.addMember(member, isStatic = false) scope.pushMethodScope(method, ExpectedType.empty, isStatic = false) @@ -59,10 +60,13 @@ class ScopeTests extends AnyFlatSpec with Matchers with BeforeAndAfterAll { val scope = new Scope() scope.pushTypeDeclScope(outerTypeDecl, isStatic = false) scope.pushMethodScope(method, ExpectedType.empty, isStatic = false) - scope.enclosingMethod.get.addParameter(outerParameter) + scope.enclosingMethod.get.addParameter(outerParameter, genericSignature) scope.pushTypeDeclScope(innerTypeDecl, isStatic = false) - scope.lookupVariable("fooParameter") shouldBe CapturedVariable(Nil, ScopeParameter(outerParameter)) + scope.lookupVariable("fooParameter") shouldBe CapturedVariable( + Nil, + ScopeParameter(outerParameter, genericSignature) + ) } it should "find a capture chain for a captured variable in an outer-outer scope" in { @@ -76,14 +80,14 @@ class ScopeTests extends AnyFlatSpec with Matchers with BeforeAndAfterAll { val scope = new Scope() scope.pushTypeDeclScope(outerOuterTypeDecl, isStatic = false) scope.pushMethodScope(outerOuterMethod, ExpectedType.empty, isStatic = false) - scope.enclosingMethod.get.addParameter(outerOuterParameter) + scope.enclosingMethod.get.addParameter(outerOuterParameter, genericSignature) scope.pushTypeDeclScope(outerTypeDecl, isStatic = false) scope.pushMethodScope(outerMethod, ExpectedType.empty, isStatic = false) scope.pushTypeDeclScope(innerTypeDecl, isStatic = false) scope.lookupVariable("parameter") shouldBe CapturedVariable( List(outerTypeDecl), - ScopeParameter(outerOuterParameter) + ScopeParameter(outerOuterParameter, genericSignature) ) } @@ -98,7 +102,7 @@ class ScopeTests extends AnyFlatSpec with Matchers with BeforeAndAfterAll { val scope = new Scope() scope.pushTypeDeclScope(outerOuterTypeDecl, isStatic = false) scope.pushMethodScope(outerOuterMethod, ExpectedType.empty, isStatic = false) - scope.enclosingMethod.get.addParameter(outerOuterParameter) + scope.enclosingMethod.get.addParameter(outerOuterParameter, genericSignature) scope.pushTypeDeclScope(outerTypeDecl, isStatic = false) scope.pushMethodScope(outerMethod, ExpectedType.empty, isStatic = false) scope.pushTypeDeclScope(innerTypeDecl, isStatic = true) diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/AstNodeBuilder.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/AstNodeBuilder.scala index 217814f6d4d0..d6635621aec4 100644 --- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/AstNodeBuilder.scala +++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/AstNodeBuilder.scala @@ -79,15 +79,18 @@ trait AstNodeBuilder[Node, NodeProcessor] { this: NodeProcessor => name: String, code: String, typeFullName: String, - dynamicTypeHints: Seq[String] = Seq() + dynamicTypeHints: Seq[String] = Seq(), + genericSignature: Option[String] = None ): NewMember = { - NewMember() + val member = NewMember() .code(code) .name(name) .typeFullName(typeFullName) .dynamicTypeHintFullName(dynamicTypeHints) .lineNumber(line(node)) .columnNumber(column(node)) + genericSignature.foreach(member.genericSignature(_)) + member } protected def newImportNode(code: String, importedEntity: String, importedAs: String, include: Node): NewImport = { @@ -140,7 +143,8 @@ trait AstNodeBuilder[Node, NodeProcessor] { this: NodeProcessor => astParentType: String = "", astParentFullName: String = "", inherits: Seq[String] = Seq.empty, - alias: Option[String] = None + alias: Option[String] = None, + genericSignature: Option[String] = None ): NewTypeDecl = { val node_ = NewTypeDecl() .name(name) @@ -157,6 +161,7 @@ trait AstNodeBuilder[Node, NodeProcessor] { this: NodeProcessor => offset(node).foreach { case (offset, offsetEnd) => node_.offset(offset).offsetEnd(offsetEnd) } + genericSignature.foreach(node_.genericSignature(_)) node_ } @@ -258,15 +263,19 @@ trait AstNodeBuilder[Node, NodeProcessor] { this: NodeProcessor => name: String, code: String, typeFullName: String, - closureBindingId: Option[String] = None - ): NewLocal = - NewLocal() + closureBindingId: Option[String] = None, + genericSignature: Option[String] = None + ): NewLocal = { + val local = NewLocal() .name(name) .code(code) .typeFullName(typeFullName) .closureBindingId(closureBindingId) .lineNumber(line(node)) .columnNumber(column(node)) + genericSignature.foreach(local.genericSignature(_)) + local + } protected def identifierNode( node: Node, @@ -296,7 +305,8 @@ trait AstNodeBuilder[Node, NodeProcessor] { this: NodeProcessor => signature: Option[String], fileName: String, astParentType: Option[String] = None, - astParentFullName: Option[String] = None + astParentFullName: Option[String] = None, + genericSignature: Option[String] = None ): NewMethod = { val node_ = NewMethod() @@ -312,6 +322,7 @@ trait AstNodeBuilder[Node, NodeProcessor] { this: NodeProcessor => .lineNumberEnd(lineEnd(node)) .columnNumberEnd(columnEnd(node)) signature.foreach { s => node_.signature(StringUtils.normalizeSpace(s)) } + genericSignature.foreach(node_.genericSignature(_)) offset(node).foreach { case (offset, offsetEnd) => node_.offset(offset).offsetEnd(offsetEnd) } diff --git a/project/Versions.scala b/project/Versions.scala index 4500a6dfb944..b7bdd352a8de 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -48,6 +48,7 @@ object Versions { val typeSafeConfig = "1.4.3" val versionSort = "1.0.11" val zip4j = "2.11.5" + val asm = "9.7.1" private def parseVersion(key: String): String = { val versionRegexp = s""".*val $key[ ]+=[ ]?"(.*?)"""".r