diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Kotlin2Cpg.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Kotlin2Cpg.scala index e9eb562a32a0..92796322bc83 100644 --- a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Kotlin2Cpg.scala +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Kotlin2Cpg.scala @@ -7,8 +7,7 @@ import io.joern.kotlin2cpg.files.SourceFilesPicker import io.joern.kotlin2cpg.interop.JavasrcInterop import io.joern.kotlin2cpg.jar4import.UsesService import io.joern.kotlin2cpg.passes.* -import io.joern.kotlin2cpg.types.ContentSourcesPicker -import io.joern.kotlin2cpg.types.DefaultTypeInfoProvider +import io.joern.kotlin2cpg.types.{ContentSourcesPicker, DefaultTypeInfoProvider, TypeRenderer} import io.joern.kotlin2cpg.utils.PathUtils import io.joern.x2cpg.SourceFiles import io.joern.x2cpg.X2CpgFrontend @@ -228,8 +227,11 @@ class Kotlin2Cpg extends X2CpgFrontend[Config] with UsesService { new MetaDataPass(cpg, Languages.KOTLIN, config.inputPath).createAndApply() + val typeRenderer = new TypeRenderer(config.keepTypeArguments) val astCreator = - new AstCreationPass(sourceFiles, new DefaultTypeInfoProvider(environment), cpg)(config.schemaValidation) + new AstCreationPass(sourceFiles, new DefaultTypeInfoProvider(environment, typeRenderer), cpg)( + config.schemaValidation + ) astCreator.createAndApply() val kotlinAstCreatorTypes = astCreator.global.usedTypes.keys().asScala.toList diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Main.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Main.scala index 59cf3540b9c8..4d20c13c1e90 100644 --- a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Main.scala +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Main.scala @@ -14,7 +14,8 @@ final case class Config( jar4importServiceUrl: Option[String] = None, includeJavaSourceFiles: Boolean = false, generateNodesForDependencies: Boolean = false, - downloadDependencies: Boolean = false + downloadDependencies: Boolean = false, + keepTypeArguments: Boolean = false ) extends X2CpgConfig[Config] with DependencyDownloadConfig[Config] { @@ -49,6 +50,10 @@ final case class Config( override def withDownloadDependencies(value: Boolean): Config = { this.copy(downloadDependencies = value).withInheritedFields(this) } + + def withKeepTypeArguments(value: Boolean): Config = { + copy(keepTypeArguments = value).withInheritedFields(this) + } } private object Frontend { @@ -82,7 +87,11 @@ private object Frontend { opt[Unit]("generate-nodes-for-dependencies") .text("Generate nodes for the dependencies of the target project") .action((_, c) => c.withGenerateNodesForDependencies(true)), - DependencyDownloadConfig.parserOptions + DependencyDownloadConfig.parserOptions, + opt[Unit]("keep-type-arguments") + .hidden() + .action((_, c) => c.withKeepTypeArguments(true)) + .text("Type full names of variables keep their type arguments.") ) } } diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/ast/AstForExpressionsCreator.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/ast/AstForExpressionsCreator.scala index f8f1da7843cb..4065bf7c2b2e 100644 --- a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/ast/AstForExpressionsCreator.scala +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/ast/AstForExpressionsCreator.scala @@ -448,14 +448,19 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { val methodFqName = if (importedNames.isDefinedAt(referencedName)) { importedNames(referencedName).getImportedFqName.toString - } else if (nameToClass.contains(expr.getCalleeExpression.getText)) { + } else if (Option(expr.getCalleeExpression).map(_.getText).exists(nameToClass.contains)) { val klass = nameToClass(expr.getCalleeExpression.getText) s"${klass.getContainingKtFile.getPackageFqName.toString}.$referencedName" } else { s"${expr.getContainingKtFile.getPackageFqName.toString}.$referencedName" } - val explicitSignature = s"${TypeConstants.any}(${argAsts.map { _ => TypeConstants.any }.mkString(",")})" - val explicitFullName = s"$methodFqName:$explicitSignature" + lazy val typeArgs = + expr.getTypeArguments.asScala.map(x => typeInfoProvider.typeFullName(x.getTypeReference, TypeConstants.any)) + val explicitSignature = s"${TypeConstants.any}(${argAsts.map { _ => TypeConstants.any }.mkString(",")})" + val explicitFullName = + if (typeInfoProvider.typeRenderer.keepTypeArguments && typeArgs.nonEmpty) + s"$methodFqName<${typeArgs.mkString(",")}>:$explicitSignature" + else s"$methodFqName:$explicitSignature" val (fullName, signature) = typeInfoProvider.fullNameWithSignature(expr, (explicitFullName, explicitSignature)) // TODO: add test case to confirm whether the ANY fallback makes sense (could be void) diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/DefaultTypeInfoProvider.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/DefaultTypeInfoProvider.scala index d05dd08ebd38..951a119dfe72 100644 --- a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/DefaultTypeInfoProvider.scala +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/DefaultTypeInfoProvider.scala @@ -66,7 +66,8 @@ import scala.jdk.CollectionConverters.CollectionHasAsScala import scala.util.control.NonFatal import scala.util.{Failure, Success, Try} -class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeInfoProvider { +class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment, typeRenderer: TypeRenderer = new TypeRenderer()) + extends TypeInfoProvider(typeRenderer) { private val logger = LoggerFactory.getLogger(getClass) import DefaultTypeInfoProvider._ @@ -107,7 +108,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def fullName(expr: KtTypeAlias, defaultValue: String): String = { val mapForEntity = bindingsForEntity(bindingContext, expr) Option(mapForEntity.get(BindingContext.TYPE_ALIAS.getKey)) - .map(TypeRenderer.renderFqNameForDesc) + .map(typeRenderer.renderFqNameForDesc) .filter(isValidRender) .getOrElse(defaultValue) } @@ -135,9 +136,9 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn Option(mapForEntity.get(BindingContext.FUNCTION.getKey)) .map { fnDesc => if (DescriptorUtils.isExtension(fnDesc)) - TypeRenderer.render(fnDesc.getExtensionReceiverParameter.getType) + typeRenderer.render(fnDesc.getExtensionReceiverParameter.getType) else - TypeRenderer.renderFqNameForDesc(fnDesc.getContainingDeclaration) + typeRenderer.renderFqNameForDesc(fnDesc.getContainingDeclaration) } .getOrElse(defaultValue) } @@ -159,7 +160,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn if (!fnDesc.isActual && fnDesc.getOverriddenDescriptors.asScala.nonEmpty) fnDesc.getOverriddenDescriptors.asScala.toList.head else fnDesc - val renderedFqName = TypeRenderer.renderFqNameForDesc(relevantDesc) + val renderedFqName = typeRenderer.renderFqNameForDesc(relevantDesc) val returnTypeFullName = renderedReturnType(relevantDesc.getOriginal) val renderedParameterTypes = @@ -185,7 +186,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def typeFullName(expr: KtDestructuringDeclarationEntry, defaultValue: String): String = { val mapForEntity = bindingsForEntity(bindingContext, expr) Option(mapForEntity.get(BindingContext.VARIABLE.getKey)) - .map { desc => TypeRenderer.render(desc.getType) } + .map { desc => typeRenderer.render(desc.getType) } .filter(isValidRender) .getOrElse(defaultValue) } @@ -193,7 +194,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def typeFullName(expr: KtTypeReference, defaultValue: String): String = { val mapForEntity = bindingsForEntity(bindingContext, expr) Option(mapForEntity.get(BindingContext.TYPE.getKey)) - .map(TypeRenderer.render(_)) + .map(typeRenderer.render(_)) .filter(isValidRender) .getOrElse(defaultValue) } @@ -217,7 +218,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn Option(mapForEntity.get(BindingContext.TYPE_ALIAS.getKey)) .map(_.getExpandedType) .filterNot(_.isInstanceOf[ErrorType]) - .map(TypeRenderer.render(_)) + .map(typeRenderer.render(_)) .filter(isValidRender) .getOrElse(defaultValue) } @@ -225,7 +226,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def returnType(expr: KtNamedFunction, defaultValue: String): String = { Option(bindingContext.get(BindingContext.FUNCTION, expr)) .map(_.getReturnType) - .map(TypeRenderer.render(_)) + .map(typeRenderer.render(_)) .filter(isValidRender) .getOrElse(defaultValue) } @@ -235,7 +236,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn Option(mapForEntity.get(BindingContext.VARIABLE.getKey)) .map(_.getType) .filterNot(_.isInstanceOf[ErrorType]) - .map(TypeRenderer.render(_)) + .map(typeRenderer.render(_)) .filter(isValidRender) .getOrElse( Option(expr.getTypeReference) @@ -250,7 +251,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val mapForEntity = bindingsForEntity(bindingContext, expr) Option(mapForEntity.get(BindingContext.CLASS.getKey)) .map(_.getDefaultType) - .map(TypeRenderer.render(_)) + .map(typeRenderer.render(_)) .getOrElse(defaultValue) } @@ -262,7 +263,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .map( _.asScala .map { superClassDesc => - TypeRenderer.render(superClassDesc.getDefaultType) + typeRenderer.render(superClassDesc.getDefaultType) } .toList ) @@ -283,7 +284,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val mapForEntity = bindingsForEntity(bindingContext, expr) val nonLocalFullName = Option(mapForEntity.get(BindingContext.CLASS.getKey)) .map(_.getDefaultType) - .map(TypeRenderer.render(_)) + .map(typeRenderer.render(_)) .filter(isValidRender) .getOrElse(defaultValue) @@ -296,7 +297,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .map { containingDecl => val idxMaybe = anonymousObjectIdx(expr) val idx = idxMaybe.map(_.toString).getOrElse("nan") - s"${TypeRenderer.renderFqNameForDesc(containingDecl.getOriginal).stripSuffix(".")}" + "$object$" + s"$idx" + s"${typeRenderer.renderFqNameForDesc(containingDecl.getOriginal).stripSuffix(".")}" + "$object$" + s"$idx" } .getOrElse(nonLocalFullName) } @@ -306,7 +307,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn fnDescMaybe .map(_.getContainingDeclaration) .map { containingDecl => - s"${TypeRenderer.renderFqNameForDesc(containingDecl.getOriginal)}.${expr.getName}" + s"${typeRenderer.renderFqNameForDesc(containingDecl.getOriginal)}.${expr.getName}" } .getOrElse(nonLocalFullName) } else nonLocalFullName @@ -321,7 +322,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val mapForEntity = bindingsForEntity(bindingContext, expr) Option(mapForEntity.get(BindingContext.VALUE_PARAMETER.getKey)) .map(_.getType) - .map(TypeRenderer.render(_)) + .map(typeRenderer.render(_)) .filter(isValidRender) .getOrElse(defaultValue) } @@ -329,7 +330,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def expressionType(expr: KtExpression, defaultValue: String): String = { Option(bindingContext.get(BindingContext.EXPRESSION_TYPE_INFO, expr)) .flatMap(tpeInfo => Option(tpeInfo.getType)) - .map(TypeRenderer.render(_)) + .map(typeRenderer.render(_)) .filter(isValidRender) .getOrElse(defaultValue) } @@ -341,7 +342,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .filter(_.nonEmpty) .map { typeArguments => val firstTypeArg = typeArguments.toList.head - val rendered = TypeRenderer.render(firstTypeArg.getType) + val rendered = typeRenderer.render(firstTypeArg.getType) val retType = expressionType(expr, TypeConstants.any) val signature = s"$retType()" val fullName = s"$rendered.${TypeConstants.classLiteralReplacementMethodName}:$signature" @@ -422,7 +423,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .mkString(",") val signature = s"$returnTypeFullName($renderedParameterTypes)" - val renderedFqName = TypeRenderer.renderFqNameForDesc(relevantDesc) + val renderedFqName = typeRenderer.renderFqNameForDesc(relevantDesc) val fullName = if (isConstructorCall(expr).getOrElse(false)) s"$renderedFqName${TypeConstants.initPrefix}:$signature" else s"$renderedFqName:$signature" @@ -439,7 +440,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn chosenAmbiguousReference .map { desc => val signature = Defines.UnresolvedSignature - val fullName = s"${TypeRenderer.renderFqNameForDesc(desc)}:$signature($numArgs)" + val fullName = s"${typeRenderer.renderFqNameForDesc(desc)}:$signature($numArgs)" (fullName, signature) } .getOrElse(defaultValue) @@ -449,14 +450,14 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def typeFullName(expr: KtBinaryExpression, defaultValue: String): String = { resolvedCallDescriptor(expr) .map(_.getOriginal) - .map { desc => TypeRenderer.render(desc.getReturnType) } + .map { desc => typeRenderer.render(desc.getReturnType) } .getOrElse(defaultValue) } def typeFullName(expr: KtAnnotationEntry, defaultValue: String): String = { Option(bindingsForEntity(bindingContext, expr)) .flatMap(_ => Option(bindingContext.get(BindingContext.ANNOTATION, expr))) - .map { desc => TypeRenderer.render(desc.getType) } + .map { desc => typeRenderer.render(desc.getType) } .getOrElse(defaultValue) } @@ -467,15 +468,15 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val renderedParameterTypes = originalDesc.getValueParameters.asScala.toSeq .map(_.getType) - .map { t => TypeRenderer.render(t) } + .map { t => typeRenderer.render(t) } .mkString(",") - val renderedReturnType = TypeRenderer.render(originalDesc.getReturnType) + val renderedReturnType = typeRenderer.render(originalDesc.getReturnType) val signature = s"$renderedReturnType($renderedParameterTypes)" val fullName = if (originalDesc.isInstanceOf[ClassConstructorDescriptorImpl]) { s"$renderedReturnType.${TypeConstants.initPrefix}:$signature" } else { - val renderedFqName = TypeRenderer.renderFqNameForDesc(originalDesc) + val renderedFqName = typeRenderer.renderFqNameForDesc(originalDesc) s"$renderedFqName:$signature" } if (!isValidRender(fullName) || !isValidRender(signature)) defaultValue @@ -487,13 +488,13 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def containingDeclFullName(expr: KtCallExpression): Option[String] = { resolvedCallDescriptor(expr) .map(_.getContainingDeclaration) - .map(TypeRenderer.renderFqNameForDesc) + .map(typeRenderer.renderFqNameForDesc) } def containingDeclType(expr: KtQualifiedExpression, defaultValue: String): String = { resolvedCallDescriptor(expr) .map(_.getContainingDeclaration) - .map(TypeRenderer.renderFqNameForDesc) + .map(typeRenderer.renderFqNameForDesc) .getOrElse(defaultValue) } @@ -536,9 +537,9 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .map(_.toList) .getOrElse(List()) if (typeUpperBounds.nonEmpty) - TypeRenderer.render(typeUpperBounds.head) + typeRenderer.render(typeUpperBounds.head) else - TypeRenderer.render(p.getOriginal.getType) + typeRenderer.render(p.getOriginal.getType) } def fullNameWithSignature(expr: KtQualifiedExpression, defaultValue: (String, String)): (String, String) = { @@ -546,7 +547,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn case Some(fnDescriptor) => val originalDesc = fnDescriptor.getOriginal - val renderedFqNameForDesc = TypeRenderer.renderFqNameForDesc(fnDescriptor) + val renderedFqNameForDesc = typeRenderer.renderFqNameForDesc(fnDescriptor) val renderedFqNameMaybe = for { extensionReceiverParam <- Option(originalDesc.getExtensionReceiverParameter) erpType = extensionReceiverParam.getType @@ -563,12 +564,12 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val rendered = if (renderedFqNameForDesc.startsWith(TypeConstants.kotlinApplyPrefix)) TypeConstants.javaLangObject else if (typeUpperBounds.size == 1) { - TypeRenderer.render( + typeRenderer.render( typeUpperBounds.head, shouldMapPrimitiveArrayTypes = false, unwrapPrimitives = false ) - } else TypeRenderer.render(erpType, shouldMapPrimitiveArrayTypes = false, unwrapPrimitives = false) + } else typeRenderer.render(erpType, shouldMapPrimitiveArrayTypes = false, unwrapPrimitives = false) s"$rendered.${originalDesc.getName}" } } @@ -579,7 +580,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .map(_.getContainingDeclaration) .map { objDesc => if (DescriptorUtils.isAnonymousObject(objDesc)) { - s"${TypeRenderer.renderFqNameForDesc(objDesc)}.${originalDesc.getName}" + s"${typeRenderer.renderFqNameForDesc(objDesc)}.${originalDesc.getName}" } else renderedFqNameMaybe.getOrElse(renderedFqNameForDesc) } .getOrElse(renderedFqNameMaybe.getOrElse(renderedFqNameForDesc)) @@ -591,7 +592,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val renderedReturnType = if (isConstructorDescriptor(originalDesc)) TypeConstants.void else if (renderedFqNameForDesc.startsWith(TypeConstants.kotlinApplyPrefix)) TypeConstants.javaLangObject - else TypeRenderer.render(originalDesc.getReturnType) + else typeRenderer.render(originalDesc.getReturnType) val singleLambdaArgExprMaybe = expr.getSelectorExpression match { case c: KtCallExpression if c.getLambdaArguments.size() == 1 => @@ -615,7 +616,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn } case _ => val originalDesc = desc.getOriginal - val lhsName = TypeRenderer.render(originalDesc.getReturnType) + val lhsName = typeRenderer.render(originalDesc.getReturnType) val name = expr.getSelectorExpression.getFirstChild.getText val numArgs = expr.getSelectorExpression match { case c: KtCallExpression => c.getValueArguments.size() @@ -657,9 +658,9 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .getOrElse(List()) render = if (typeUpperBounds.nonEmpty) - TypeRenderer.render(typeUpperBounds.head) + typeRenderer.render(typeUpperBounds.head) else - TypeRenderer.render(variableDesc.getType) + typeRenderer.render(variableDesc.getType) if isValidRender(render) && !variableDesc.getType.isInstanceOf[ErrorType] } yield render @@ -676,7 +677,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val render = for { mapForEntity <- Option(bindingsForEntity(bindingContext, expr)) variableDesc <- Option(mapForEntity.get(BindingContext.VARIABLE.getKey)) - render = TypeRenderer.render(variableDesc.getType) + render = typeRenderer.render(variableDesc.getType) if isValidRender(render) && !variableDesc.getType.isInstanceOf[ErrorType] } yield render render.getOrElse(defaultValue) @@ -687,7 +688,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn case callExpr: KtCallExpression => resolvedCallDescriptor(callExpr) match { case Some(desc) => - val rendered = TypeRenderer.renderFqNameForDesc(desc.getOriginal) + val rendered = typeRenderer.renderFqNameForDesc(desc.getOriginal) rendered.startsWith(TypeConstants.kotlinApplyPrefix) || rendered.startsWith(TypeConstants.kotlinAlsoPrefix) case _ => false } @@ -717,7 +718,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val renderedRetType = args.lastOption - .map { t => TypeRenderer.render(t.getType) } + .map { t => typeRenderer.render(t.getType) } .getOrElse(TypeConstants.javaLangObject) val renderedArgs = if (args.isEmpty) "" @@ -739,14 +740,14 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn if (hasReturnTypeFromTypeParams) { if (returnT.getConstructor.getSupertypes.asScala.nonEmpty) { val firstSuperType = returnT.getConstructor.getSupertypes.asScala.toList.head - TypeRenderer.render(firstSuperType) + typeRenderer.render(firstSuperType) } else { - val renderedReturnT = TypeRenderer.render(returnT) + val renderedReturnT = typeRenderer.render(returnT) if (renderedReturnT == TypeConstants.tType) TypeConstants.javaLangObject else renderedReturnT } } else { - TypeRenderer.render(fnDesc.getReturnType) + typeRenderer.render(fnDesc.getReturnType) } } @@ -756,14 +757,14 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val explicitTypeFullName = Option(parameter.getTypeReference) .map(_.getText) - .map(TypeRenderer.stripped) + .map(typeRenderer.stripped) .getOrElse(Defines.UnresolvedNamespace) // TODO: return all the parameter types in this fn for registration, otherwise they will be missing parameterType(parameter, explicitTypeFullName) } val paramListSignature = s"(${paramTypeNames.mkString(",")})" val methodName = fnDesc - .map(desc => s"${TypeRenderer.renderFqNameForDesc(desc)}${TypeConstants.initPrefix}") + .map(desc => s"${typeRenderer.renderFqNameForDesc(desc)}${TypeConstants.initPrefix}") .getOrElse(s"${Defines.UnresolvedNamespace}.${TypeConstants.initPrefix}") val signature = s"${TypeConstants.void}$paramListSignature" val fullname = s"$methodName:$signature" @@ -780,12 +781,12 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .map(_.getText) .getOrElse(Defines.UnresolvedNamespace) // TODO: return all the parameter types in this fn for registration, otherwise they will be missing - parameterType(parameter, TypeRenderer.stripped(explicitTypeFullName)) + parameterType(parameter, typeRenderer.stripped(explicitTypeFullName)) } val paramListSignature = s"(${paramTypeNames.mkString(",")})" val methodName = Option(bindingContext.get(BindingContext.CONSTRUCTOR, expr)) - .map { info => s"${TypeRenderer.renderFqNameForDesc(info)}${TypeConstants.initPrefix}" } + .map { info => s"${typeRenderer.renderFqNameForDesc(info)}${TypeConstants.initPrefix}" } .getOrElse(s"${Defines.UnresolvedNamespace}.${TypeConstants.initPrefix}") val signature = s"${TypeConstants.void}$paramListSignature" val fullname = s"$methodName:$signature" @@ -810,7 +811,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn val renderedRetType = args.lastOption - .map { t => TypeRenderer.render(t.getType) } + .map { t => typeRenderer.render(t.getType) } .getOrElse(TypeConstants.javaLangObject) val renderedArgs = if (args.isEmpty) "" @@ -832,7 +833,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn .map(_.getText) .getOrElse(Defines.UnresolvedNamespace) // TODO: return all the parameter types in this fn for registration, otherwise they will be missing - parameterType(parameter, TypeRenderer.stripped(explicitTypeFullName)) + parameterType(parameter, typeRenderer.stripped(explicitTypeFullName)) } val paramListSignature = s"(${paramTypeNames.mkString(",")})" @@ -845,7 +846,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn s"${Defines.UnresolvedNamespace}.${expr.getName}" } else { val theType = fnDescMaybe.get.getExtensionReceiverParameter.getType - val renderedType = TypeRenderer.render(theType) + val renderedType = typeRenderer.render(theType) s"$renderedType.${expr.getName}" } } @@ -855,7 +856,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn fnDescMaybe .map(_.getContainingDeclaration) .map { containingDecl => - s"${TypeRenderer.renderFqNameForDesc(containingDecl.getOriginal)}.${expr.getName}" + s"${typeRenderer.renderFqNameForDesc(containingDecl.getOriginal)}.${expr.getName}" } .getOrElse(nameNoParent) } else nameNoParent @@ -879,7 +880,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def referenceTargetTypeFullName(expr: KtNameReferenceExpression, defaultValue: String): String = { descriptorForNameReference(expr) - .collect { case desc: PropertyDescriptorImpl => TypeRenderer.renderFqNameForDesc(desc.getContainingDeclaration) } + .collect { case desc: PropertyDescriptorImpl => typeRenderer.renderFqNameForDesc(desc.getContainingDeclaration) } .getOrElse(defaultValue) } @@ -902,26 +903,26 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn def typeFullName(expr: KtPrimaryConstructor, defaultValue: String): String = { Option(bindingContext.get(BindingContext.CONSTRUCTOR, expr)) - .map { desc => TypeRenderer.render(desc.getReturnType) } + .map { desc => typeRenderer.render(desc.getReturnType) } .getOrElse(defaultValue) } def typeFullName(expr: KtSecondaryConstructor, defaultValue: String): String = { Option(bindingContext.get(BindingContext.CONSTRUCTOR, expr)) - .map { desc => TypeRenderer.render(desc.getReturnType) } + .map { desc => typeRenderer.render(desc.getReturnType) } .getOrElse(defaultValue) } def typeFullName(expr: KtNameReferenceExpression, defaultValue: String): String = { descriptorForNameReference(expr) .flatMap { - case typedDesc: ValueDescriptor => Some(TypeRenderer.render(typedDesc.getType)) + case typedDesc: ValueDescriptor => Some(typeRenderer.render(typedDesc.getType)) // TODO: add test cases for the LazyClassDescriptors (`okio` codebase serves as good example) - case typedDesc: LazyClassDescriptor => Some(TypeRenderer.render(typedDesc.getDefaultType)) - case typedDesc: LazyJavaClassDescriptor => Some(TypeRenderer.render(typedDesc.getDefaultType)) - case typedDesc: DeserializedClassDescriptor => Some(TypeRenderer.render(typedDesc.getDefaultType)) - case typedDesc: EnumEntrySyntheticClassDescriptor => Some(TypeRenderer.render(typedDesc.getDefaultType)) - case typedDesc: LazyPackageViewDescriptorImpl => Some(TypeRenderer.renderFqNameForDesc(typedDesc)) + case typedDesc: LazyClassDescriptor => Some(typeRenderer.render(typedDesc.getDefaultType)) + case typedDesc: LazyJavaClassDescriptor => Some(typeRenderer.render(typedDesc.getDefaultType)) + case typedDesc: DeserializedClassDescriptor => Some(typeRenderer.render(typedDesc.getDefaultType)) + case typedDesc: EnumEntrySyntheticClassDescriptor => Some(typeRenderer.render(typedDesc.getDefaultType)) + case typedDesc: LazyPackageViewDescriptorImpl => Some(typeRenderer.renderFqNameForDesc(typedDesc)) case unhandled: Any => logger.debug(s"Unhandled class type info fetch in for `${expr.getText}` with class `${unhandled.getClass}`.") None @@ -960,7 +961,7 @@ class DefaultTypeInfoProvider(environment: KotlinCoreEnvironment) extends TypeIn case Some(fnDescriptor) => val originalDesc = fnDescriptor.getOriginal val vps = originalDesc.getValueParameters - val renderedFqName = TypeRenderer.renderFqNameForDesc(originalDesc) + val renderedFqName = typeRenderer.renderFqNameForDesc(originalDesc) if ( hasSingleImplicitParameter && (renderedFqName.startsWith(TypeConstants.kotlinRunPrefix) || diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/TypeInfoProvider.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/TypeInfoProvider.scala index dc878bbfa323..31be4b611743 100644 --- a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/TypeInfoProvider.scala +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/TypeInfoProvider.scala @@ -26,7 +26,7 @@ import org.jetbrains.kotlin.psi.{ case class AnonymousObjectContext(declaration: KtElement) -trait TypeInfoProvider { +trait TypeInfoProvider(val typeRenderer: TypeRenderer = new TypeRenderer()) { def isExtensionFn(fn: KtNamedFunction): Boolean def usedAsExpression(expr: KtExpression): Option[Boolean] diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/TypeRenderer.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/TypeRenderer.scala index 716c5e0a0631..c0a6f6ff5989 100644 --- a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/TypeRenderer.scala +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/types/TypeRenderer.scala @@ -12,6 +12,8 @@ import org.jetbrains.kotlin.renderer.{DescriptorRenderer, DescriptorRendererImpl import org.jetbrains.kotlin.types.typeUtil.TypeUtilsKt import org.jetbrains.kotlin.resolve.jvm.JvmPrimitiveType +import scala.jdk.CollectionConverters.* + object TypeRenderer { private val cpgUnresolvedType = @@ -28,6 +30,12 @@ object TypeRenderer { "kotlin.ShortArray" -> "short[]" ) +} + +class TypeRenderer(val keepTypeArguments: Boolean = false) { + + import TypeRenderer.* + private def descriptorRenderer(): DescriptorRenderer = { val opts = new DescriptorRendererOptionsImpl opts.setParameterNamesInFunctionalTypes(false) @@ -130,9 +138,20 @@ object TypeRenderer { val relevantT = Option(TypeUtilsKt.getImmediateSuperclassNotAny(t)).getOrElse(t) stripped(renderer.renderType(relevantT)) } - if (shouldMapPrimitiveArrayTypes && primitiveArrayMappings.contains(rendered)) primitiveArrayMappings(rendered) - else if (rendered == TypeConstants.kotlinUnit) TypeConstants.void - else rendered + val renderedType = + if (shouldMapPrimitiveArrayTypes && primitiveArrayMappings.contains(rendered)) primitiveArrayMappings(rendered) + else if (rendered == TypeConstants.kotlinUnit) TypeConstants.void + else rendered + + if (keepTypeArguments && !t.getArguments.isEmpty) { + val typeArgs = t.getArguments.asScala + .map(_.getType) + .map(render(_, shouldMapPrimitiveArrayTypes, unwrapPrimitives)) + .mkString(",") + s"$renderedType<$typeArgs>" + } else { + renderedType + } } private def isFunctionXType(t: KotlinType): Boolean = { diff --git a/joern-cli/frontends/kotlin2cpg/src/test/scala/io/joern/kotlin2cpg/querying/TypeTests.scala b/joern-cli/frontends/kotlin2cpg/src/test/scala/io/joern/kotlin2cpg/querying/TypeTests.scala index f6c6dde3fd9e..e529c84539ba 100644 --- a/joern-cli/frontends/kotlin2cpg/src/test/scala/io/joern/kotlin2cpg/querying/TypeTests.scala +++ b/joern-cli/frontends/kotlin2cpg/src/test/scala/io/joern/kotlin2cpg/querying/TypeTests.scala @@ -1,7 +1,9 @@ package io.joern.kotlin2cpg.querying +import io.joern.kotlin2cpg.Config import io.joern.kotlin2cpg.testfixtures.KotlinCode2CpgFixture -import io.shiftleft.semanticcpg.language._ +import io.shiftleft.codepropertygraph.generated.Operators +import io.shiftleft.semanticcpg.language.* class TypeTests extends KotlinCode2CpgFixture(withOssDataflow = false) { @@ -64,4 +66,48 @@ class TypeTests extends KotlinCode2CpgFixture(withOssDataflow = false) { x.name shouldBe "l" } } + + "generics with 'keep type arguments' config" should { + + "show the fully qualified type arguments for stdlib `List and `Map` objects" in { + val cpg = code(""" + |import java.util.ArrayList + |import java.util.HashMap + | + |fun foo() { + | val stringList = ArrayList() + | val stringIntMap = HashMap() + |} + |""".stripMargin) + .withConfig(Config().withKeepTypeArguments(true)) + + cpg.identifier("stringList").typeFullName.head shouldBe "java.util.ArrayList" + cpg.identifier("stringIntMap").typeFullName.head shouldBe "java.util.HashMap" + } + + "show the fully qualified names of external types" in { + val cpg = code(""" + |import org.apache.flink.streaming.api.datastream.DataStream + |import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment + |import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer + |import org.apache.flink.streaming.util.serialization.SimpleStringSchema + | + |import java.util.Properties; + | + |class FlinkKafkaExample { + | fun main() { + | val kafkaProducer = FlinkKafkaProducer("kafka-topic") + | } + |} + |""".stripMargin).withConfig(Config().withKeepTypeArguments(true)) + + cpg.call + .codeExact("FlinkKafkaProducer(\"kafka-topic\")") + .filterNot(_.name == Operators.alloc) + .map(_.methodFullName) + .head shouldBe "org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer:ANY(ANY)" + } + + } + } diff --git a/joern-cli/frontends/kotlin2cpg/src/test/scala/io/joern/kotlin2cpg/testfixtures/KotlinCodeToCpgFixture.scala b/joern-cli/frontends/kotlin2cpg/src/test/scala/io/joern/kotlin2cpg/testfixtures/KotlinCodeToCpgFixture.scala index afe8dbc0c4d1..31dc5646a73c 100644 --- a/joern-cli/frontends/kotlin2cpg/src/test/scala/io/joern/kotlin2cpg/testfixtures/KotlinCodeToCpgFixture.scala +++ b/joern-cli/frontends/kotlin2cpg/src/test/scala/io/joern/kotlin2cpg/testfixtures/KotlinCodeToCpgFixture.scala @@ -19,15 +19,22 @@ trait KotlinFrontend extends LanguageFrontend { protected val withTestResourcePaths: Boolean override val fileSuffix: String = ".kt" + private lazy val defaultContentRoot = + BFile(ProjectRoot.relativise("joern-cli/frontends/kotlin2cpg/src/test/resources/jars/")) + private lazy val defaultConfig: Config = + Config( + classpath = if (withTestResourcePaths) Set(defaultContentRoot.path.toAbsolutePath.toString) else Set(), + includeJavaSourceFiles = true + ) override def execute(sourceCodeFile: File): Cpg = { - val defaultContentRoot = - BFile(ProjectRoot.relativise("joern-cli/frontends/kotlin2cpg/src/test/resources/jars/")) - implicit val defaultConfig: Config = - Config( - classpath = if (withTestResourcePaths) Set(defaultContentRoot.path.toAbsolutePath.toString) else Set(), - includeJavaSourceFiles = true - ) + implicit val config: Config = getConfig() match { + case Some(config: Config) => config + case _ => + setConfig(defaultConfig) + defaultConfig + } + new Kotlin2Cpg().createCpg(sourceCodeFile.getAbsolutePath).get } }