All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.joern.kotlin2cpg.Kotlin2Cpg.scala Maven / Gradle / Ivy

The newest version!
package io.joern.kotlin2cpg

import better.files.File
import io.joern.kotlin2cpg.compiler.CompilerAPI
import io.joern.kotlin2cpg.compiler.ErrorLoggingMessageCollector
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, DefaultTypeInfoProvider, TypeRenderer}
import io.joern.x2cpg.SourceFiles
import io.joern.x2cpg.X2CpgFrontend
import io.joern.x2cpg.X2Cpg.withNewEmptyCpg
import io.joern.x2cpg.passes.frontend.MetaDataPass
import io.joern.x2cpg.passes.frontend.TypeNodePass
import io.joern.x2cpg.utils.dependency.DependencyResolver
import io.joern.x2cpg.utils.dependency.DependencyResolverParams
import io.joern.x2cpg.utils.dependency.GradleConfigKeys
import io.joern.x2cpg.SourceFiles.filterFile
import io.shiftleft.codepropertygraph.generated.Cpg
import io.shiftleft.codepropertygraph.generated.Languages
import io.shiftleft.utils.IOUtils
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.psi.KtFile
import org.slf4j.LoggerFactory

import java.nio.file.Files
import java.nio.file.Paths
import scala.jdk.CollectionConverters.CollectionHasAsScala
import scala.jdk.CollectionConverters.EnumerationHasAsScala
import scala.util.Try
import scala.util.matching.Regex

object Kotlin2Cpg {

  private val logger = LoggerFactory.getLogger(getClass)

  private val parsingError: String = "KOTLIN2CPG_PARSING_ERROR"
  private val jarExtension: String = ".jar"
  private val importRegex: Regex   = ".*import([^;]*).*".r

  private val defaultKotlinStdlibContentRootJarPaths = Seq(
    DefaultContentRootJarPath("jars/kotlin-stdlib-1.9.0.jar", isResource = true),
    DefaultContentRootJarPath("jars/kotlin-stdlib-common-1.9.0.jar", isResource = true),
    DefaultContentRootJarPath("jars/kotlin-stdlib-jdk8-1.9.0.jar", isResource = true)
  )

  case class InputPair(content: String, fileName: String)
  type InputProvider = () => InputPair

  def postProcessingPass(cpg: Cpg): Unit = {
    new KotlinTypeRecoveryPassGenerator(cpg).generate().foreach(_.createAndApply())
    new KotlinTypeHintCallLinker(cpg).createAndApply()
  }

}

case class KtFileWithMeta(f: KtFile, relativizedPath: String, filename: String)
case class FileContentAtPath(content: String, relativizedPath: String, filename: String)

class Kotlin2Cpg extends X2CpgFrontend[Config] with UsesService {

  import Kotlin2Cpg.*

  private def checkSourceDir(sourceDir: String): Unit = {
    if (!Files.exists(Paths.get(sourceDir))) {
      println(s"The specified input path `$sourceDir` is not a file that exists. Exiting.")
      System.exit(1)
    }
    if (!Files.isDirectory(Paths.get(sourceDir))) {
      println(s"The specified input path `$sourceDir` is not a directory. Exiting.")
      System.exit(1)
    }
  }

  private def logMaxHeapSize(): Unit = {
    val maxHeapSize          = Runtime.getRuntime.maxMemory()
    val formattedMaxHeapSize = String.format("%,.2f", maxHeapSize / (1024 * 1024 * 1024).toDouble)
    logger.info(s"Max heap size currently set to `${formattedMaxHeapSize}GB`.")
  }

  private def gatherFilesWithKtExtension(sourceDir: String, config: Config): List[String] = {
    val filesWithKtExtension = SourceFiles.determine(
      sourceDir,
      Set(".kt"),
      ignoredFilesRegex = Option(config.ignoredFilesRegex),
      ignoredFilesPath = Option(config.ignoredFiles)
    )
    if (filesWithKtExtension.isEmpty) {
      println(s"The provided input directory does not contain files ending in '.kt' `$sourceDir`. Exiting.")
      System.exit(1)
    }
    filesWithKtExtension
  }

  private def gatherFilesWithJavaExtension(sourceDir: String, config: Config): List[String] = {
    val filesWithJavaExtension = SourceFiles.determine(
      sourceDir,
      Set(".java"),
      ignoredFilesRegex = Option(config.ignoredFilesRegex),
      ignoredFilesPath = Option(config.ignoredFiles)
    )
    if (filesWithJavaExtension.nonEmpty) {
      logger.info(s"Found ${filesWithJavaExtension.size} files with the `.java` extension.")
    }
    filesWithJavaExtension
  }

  private def gatherDependenciesPaths(
    sourceDir: String,
    config: Config,
    filesWithJavaExtension: List[String]
  ): Seq[String] = {
    val jar4ImportServiceOpt = config.jar4importServiceUrl.flatMap(reachableServiceMaybe)
    if (jar4ImportServiceOpt.isDefined) {
      val filesWithKtExtension = gatherFilesWithKtExtension(sourceDir, config)
      val importNames          = importNamesForFilesAtPaths(filesWithKtExtension ++ filesWithJavaExtension)
      logger.trace(s"Found imports: `$importNames`")
      dependenciesFromService(jar4ImportServiceOpt.get, importNames)
    } else if (config.downloadDependencies) {
      downloadDependencies(sourceDir, config)
    } else {
      logger.info(s"Not downloading any dependencies.")
      Seq()
    }
  }

  private def gatherMavenCoordinates(sourceDir: String, config: Config): Seq[String] = {
    if (config.generateNodesForDependencies) {
      logger.info(s"Fetching maven coordinates.")
      fetchMavenCoordinates(sourceDir, config)
    } else Seq()
  }

  private def gatherJarsAtConfigClassPath(sourceDir: String, config: Config): Seq[String] = {
    val jarsAtConfigClassPath = findJarsIn(config.classpath)
    if (config.classpath.nonEmpty) {
      if (jarsAtConfigClassPath.nonEmpty) {
        logger.info(s"Found ${jarsAtConfigClassPath.size} jars in the specified classpath.")
      } else {
        logger.warn("No jars found in the specified classpath.")
      }
    }
    jarsAtConfigClassPath
  }

  private def gatherDefaultContentRootJars(
    sourceDir: String,
    config: Config,
    filesWithJavaExtension: List[String]
  ): Seq[DefaultContentRootJarPath] = {
    val stdlibJars            = if (config.withStdlibJarsInClassPath) defaultKotlinStdlibContentRootJarPaths else Seq()
    val jarsAtConfigClassPath = gatherJarsAtConfigClassPath(sourceDir, config)
    val dependenciesPaths     = gatherDependenciesPaths(sourceDir, config, filesWithJavaExtension)
    val defaultContentRootJars = stdlibJars ++
      jarsAtConfigClassPath.map { path => DefaultContentRootJarPath(path, isResource = false) } ++
      dependenciesPaths.map { path =>
        DefaultContentRootJarPath(path, isResource = false)
      }
    defaultContentRootJars
  }

  private def gatherDirsForSourcesToCompile(sourceDir: String): Seq[String] = {
    val dirsForSourcesToCompile = ContentSourcesPicker.dirsForRoot(sourceDir)
    if (dirsForSourcesToCompile.isEmpty) {
      logger.warn("The list of directories to analyze is empty.")
    }
    dirsForSourcesToCompile
  }

  private def gatherSourceFiles(
    sourceDir: String,
    config: Config,
    environment: KotlinCoreEnvironment
  ): Iterable[KtFileWithMeta] = {
    val sourceEntries = entriesForSources(environment.getSourceFiles.asScala, sourceDir)
    val sourceFiles = sourceEntries.filter(entry =>
      SourceFiles.filterFile(
        entry.filename,
        config.inputPath,
        ignoredFilesRegex = Option(config.ignoredFilesRegex),
        ignoredFilesPath = Option(config.ignoredFiles)
      )
    )
    sourceFiles
  }

  private def runJavasrcInterop(
    cpg: Cpg,
    sourceDir: String,
    config: Config,
    filesWithJavaExtension: List[String],
    kotlinAstCreatorTypes: List[String]
  ): Unit = {
    if (config.includeJavaSourceFiles && filesWithJavaExtension.nonEmpty) {
      val javaAstCreator = JavasrcInterop.astCreationPass(config.inputPath, filesWithJavaExtension, cpg)
      javaAstCreator.createAndApply()
      val javaAstCreatorTypes = javaAstCreator.global.usedTypes.keys().asScala.toList
      TypeNodePass
        .withRegisteredTypes((javaAstCreatorTypes.toSet -- kotlinAstCreatorTypes.toSet).toList, cpg)
        .createAndApply()
    }
  }

  def createCpg(config: Config): Try[Cpg] = {
    withNewEmptyCpg(config.outputPath, config) { (cpg, config) =>
      val sourceDir = config.inputPath
      logger.info(s"Starting CPG generation for input directory `$sourceDir`.")

      checkSourceDir(sourceDir)
      logMaxHeapSize()

      val filesWithJavaExtension  = gatherFilesWithJavaExtension(sourceDir, config)
      val mavenCoordinates        = gatherMavenCoordinates(sourceDir, config)
      val defaultContentRootJars  = gatherDefaultContentRootJars(sourceDir, config, filesWithJavaExtension)
      val dirsForSourcesToCompile = gatherDirsForSourcesToCompile(sourceDir)
      val environment = CompilerAPI.makeEnvironment(
        dirsForSourcesToCompile,
        filesWithJavaExtension,
        defaultContentRootJars,
        new ErrorLoggingMessageCollector
      )

      val sourceFiles = gatherSourceFiles(sourceDir, config, environment)
      val configFiles = entriesForConfigFiles(SourceFilesPicker.configFiles(sourceDir), sourceDir)

      new MetaDataPass(cpg, Languages.KOTLIN, config.inputPath).createAndApply()

      val typeRenderer = new TypeRenderer(config.keepTypeArguments)
      val astCreator = new AstCreationPass(sourceFiles, new DefaultTypeInfoProvider(environment, typeRenderer), cpg)(
        config.schemaValidation
      )
      astCreator.createAndApply()

      val kotlinAstCreatorTypes = astCreator.usedTypes()
      TypeNodePass.withRegisteredTypes(kotlinAstCreatorTypes, cpg).createAndApply()

      runJavasrcInterop(cpg, sourceDir, config, filesWithJavaExtension, kotlinAstCreatorTypes)
      new ConfigPass(configFiles, cpg).createAndApply()
      new DependenciesFromMavenCoordinatesPass(mavenCoordinates, cpg).createAndApply()
    }
  }

  private def importNamesForFilesAtPaths(paths: Seq[String]): Seq[String] = {
    paths.flatMap(File(_).lines.filter(_.startsWith("import")).toSeq).map(importRegex.replaceAllIn(_, "$1").trim)
  }

  private def gatherGradleParams(config: Config) = {
    Map(
      GradleConfigKeys.ProjectName       -> config.gradleProjectName,
      GradleConfigKeys.ConfigurationName -> config.gradleConfigurationName
    ).collect { case (key, Some(value)) => (key, value) }
  }

  private def downloadDependencies(sourceDir: String, config: Config): Seq[String] = {
    val gradleParams   = gatherGradleParams(config)
    val resolverParams = DependencyResolverParams(Map.empty, gradleParams)

    DependencyResolver.getDependencies(Paths.get(sourceDir), resolverParams) match {
      case Some(deps) =>
        logger.info(s"Using ${deps.size} dependency jars.")
        deps
      case None =>
        logger.warn(s"Could not fetch dependencies for project at path $sourceDir")
        println("Could not fetch dependencies when explicitly asked to.")
        Seq()
    }
  }

  private def fetchMavenCoordinates(sourceDir: String, config: Config): Seq[String] = {
    val gradleParams   = gatherGradleParams(config)
    val resolverParams = DependencyResolverParams(Map.empty, gradleParams)

    DependencyResolver.getCoordinates(Paths.get(sourceDir), resolverParams) match {
      case Some(coordinates) =>
        logger.info(s"Found ${coordinates.size} maven coordinates.")
        coordinates.toSeq
      case None =>
        logger.warn(s"Could not fetch coordinates for project at path $sourceDir")
        println("Could not fetch coordinates when explicitly asked to.")
        Seq()
    }
  }

  private def findJarsIn(dirs: Set[String]) = {
    dirs.foldLeft(Seq[String]())((acc, classpathEntry) => {
      val f = File(classpathEntry)
      val files =
        if (f.isDirectory) f.listRecursively.filter(_.extension.getOrElse("") == jarExtension).map(_.toString)
        else Seq()
      acc ++ files
    })
  }

  private def entriesForSources(files: Iterable[KtFile], relativeTo: String): Iterable[KtFileWithMeta] = {
    val filesWithMeta = for {
      file    <- files
      relPath <- Try(SourceFiles.toRelativePath(file.getVirtualFilePath, relativeTo)).toOption
    } yield KtFileWithMeta(file, relPath, file.getVirtualFilePath)
    filesWithMeta.filterNot { fwp =>
      // TODO: add test for this type of filtering
      // TODO: support Windows paths
      val willFilter = SourceFilesPicker.shouldFilter(fwp.relativizedPath)
      if (willFilter) {
        logger.debug(s"Filtered file at `${fwp.f.getVirtualFilePath}`.")
      }
      willFilter
    }
  }

  private def entriesForConfigFiles(paths: Seq[String], sourceDir: String): Seq[FileContentAtPath] = {
    for {
      fileName     <- paths
      relPath      <- Try(SourceFiles.toRelativePath(fileName, sourceDir)).toOption
      fileContents <- Try(IOUtils.readEntireFile(Paths.get(fileName))).toOption
    } yield FileContentAtPath(fileContents, relPath, fileName)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy