Skip to content

Commit

Permalink
Introduce JavaSummaryTypeSolver
Browse files Browse the repository at this point in the history
  • Loading branch information
xavierpinho committed Mar 27, 2024
1 parent 93283b8 commit 9c6da7c
Show file tree
Hide file tree
Showing 6 changed files with 533 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ final case class Config(
showEnv: Boolean = false,
skipTypeInfPass: Boolean = false,
dumpJavaparserAsts: Boolean = false,
cacheJdkTypeSolver: Boolean = false
cacheJdkTypeSolver: Boolean = false,
typeSummariesPath: Option[String] = None
) extends X2CpgConfig[Config]
with TypeRecoveryParserConfig[Config] {
def withInferenceJarPaths(paths: Set[String]): Config = {
Expand Down Expand Up @@ -67,6 +68,10 @@ final case class Config(
def withCacheJdkTypeSolver(value: Boolean): Config = {
copy(cacheJdkTypeSolver = value).withInheritedFields(this)
}

def withTypeSummariesPath(path: String): Config = {
copy(typeSummariesPath = Some(path)).withInheritedFields(this)
}
}

private object Frontend {
Expand Down Expand Up @@ -120,7 +125,11 @@ private object Frontend {
opt[Unit]("cache-jdk-type-solver")
.hidden()
.action((_, c) => c.withCacheJdkTypeSolver(true))
.text("Re-use JDK type solver between scans.")
.text("Re-use JDK type solver between scans."),
opt[String]("type-summaries-path")
.hidden()
.action((path, c) => c.withTypeSummariesPath(path))
.text("Type summaries path used for resolving external types.")
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
package io.joern.javasrc2cpg.passes

import better.files.File
import com.github.javaparser.{JavaParser, ParserConfiguration}
import com.github.javaparser.ParserConfiguration.LanguageLevel
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.Node.Parsedness
import com.github.javaparser.symbolsolver.JavaSymbolSolver
import com.github.javaparser.symbolsolver.resolution.typesolvers.{
ClassLoaderTypeSolver,
JarTypeSolver,
ReflectionTypeSolver
}
import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver
import io.joern.javasrc2cpg.JavaSrc2Cpg.JavaSrcEnvVar
import io.joern.javasrc2cpg.astcreation.AstCreator
import io.joern.javasrc2cpg.passes.AstCreationPass.*
import io.joern.javasrc2cpg.typesolvers.{EagerSourceTypeSolver, JdkJarTypeSolver, SimpleCombinedTypeSolver}
import io.joern.javasrc2cpg.util.Delombok.DelombokMode
import io.joern.javasrc2cpg.util.Delombok.DelombokMode.*
import io.joern.javasrc2cpg.typesolvers.*
import io.joern.javasrc2cpg.util.{Delombok, SourceParser}
import io.joern.javasrc2cpg.{Config, JavaSrc2Cpg}
import io.joern.x2cpg.SourceFiles
import io.joern.x2cpg.datastructures.Global
import io.joern.x2cpg.passes.frontend.XTypeRecoveryConfig
import io.joern.x2cpg.utils.dependency.DependencyResolver
import io.shiftleft.codepropertygraph.Cpg
import io.shiftleft.passes.ConcurrentWriterCpgPass
import org.slf4j.LoggerFactory

import java.net.URLClassLoader
import java.nio.file.{Path, Paths}
import scala.collection.parallel.CollectionConverters.*
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.RichOptional
import java.nio.file.Paths
import scala.util.{Success, Try}

class AstCreationPass(config: Config, cpg: Cpg, sourcesOverride: Option[List[String]] = None)
Expand Down Expand Up @@ -112,7 +96,14 @@ class AstCreationPass(config: Config, cpg: Cpg, sourcesOverride: Option[List[Str
val sourceTypeSolver =
EagerSourceTypeSolver(sourceParser, combinedTypeSolver, symbolSolver)

val summaryTypeSolver = JavaSummaryTypeSolver(
JavaProgramSummary(config.typeSummariesPath.flatMap(JavaProgramSummary.fromJsonDirectoryPath).toList),
combinedTypeSolver,
symbolSolver
)

combinedTypeSolver.addCachingTypeSolver(sourceTypeSolver)
combinedTypeSolver.addCachingTypeSolver(summaryTypeSolver)

// Add solvers for inference jars
val jarsList = config.inferenceJarPaths.flatMap(recursiveJarsFromPath).toList
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package io.joern.javasrc2cpg.typesolvers

import better.files.File
import com.github.javaparser.ParserConfiguration.LanguageLevel
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.Node.Parsedness
import com.github.javaparser.ast.body.TypeDeclaration
import com.github.javaparser.resolution.TypeSolver
import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration
import com.github.javaparser.resolution.model.SymbolReference
import com.github.javaparser.symbolsolver.JavaSymbolSolver
import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade
import com.github.javaparser.{JavaParser, ParserConfiguration}
import io.joern.x2cpg.datastructures.{FieldLike, MethodLike, ProgramSummary, TypeLike}
import org.slf4j.LoggerFactory
import upickle.default.*

import java.io.{ByteArrayInputStream, FileInputStream, InputStream, StringReader}
import scala.annotation.targetName
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.RichOptional
import scala.util.{Failure, Success, Try}

/** A type solver built out of a [[JavaProgramSummary]].
*
* Proceeds similarly to [[EagerSourceTypeSolver]], by first transforming type summaries into equivalent Java
* declarations to be then parsed.
*/
class JavaSummaryTypeSolver(
summary: JavaProgramSummary,
combinedTypeSolver: SimpleCombinedTypeSolver,
symbolSolver: JavaSymbolSolver
) extends TypeSolver {

private val logger = LoggerFactory.getLogger(getClass)
private var parent: TypeSolver = _
private val javaParser: JavaParser = new JavaParser(
new ParserConfiguration()
.setLanguageLevel(LanguageLevel.BLEEDING_EDGE)
.setStoreTokens(false)
)

private def parseSummary(javaCode: String): Option[CompilationUnit] = {
val parseResult = javaParser.parse(StringReader(javaCode))
parseResult.getResult.toScala match {
case Some(result) if result.getParsed == Parsedness.PARSED => Some(result)
case _ =>
logger.warn(s"Encountered problems while parsing a Java type summary:")
parseResult.getProblems.asScala.foreach(problem => logger.warn(s"- ${problem.getMessage}"))
None
}
}

private def resolveSymbols(cu: CompilationUnit): List[(String, SymbolReference[ResolvedReferenceTypeDeclaration])] = {
symbolSolver.inject(cu)
cu.findAll(classOf[TypeDeclaration[_]])
.asScala
.map { typeDeclaration =>
val name = typeDeclaration.getFullyQualifiedName.toScala.getOrElse(typeDeclaration.getNameAsString)
val resolvedSymbol = Try(
SymbolReference.solved(
JavaParserFacade.get(combinedTypeSolver).getTypeDeclaration(typeDeclaration)
): SymbolReference[ResolvedReferenceTypeDeclaration]
).getOrElse(SymbolReference.unsolved())
name -> resolvedSymbol
}
.toList
}

private val foundTypes: Map[String, SymbolReference[ResolvedReferenceTypeDeclaration]] =
summary.asJavaCode.flatMap(parseSummary).flatMap(resolveSymbols).toMap

override def tryToSolveType(name: String): SymbolReference[ResolvedReferenceTypeDeclaration] =
foundTypes.getOrElse(name, SymbolReference.unsolved())

override def getParent: TypeSolver = parent

override def setParent(parent: TypeSolver): Unit = {
if (parent == null) {
logger.warn(s"Cannot set parent of type solver to null. setParent will be ignored.")
} else if (this.parent != null) {
logger.warn(s"Attempting to re-set type solver parent. setParent will be ignored.")
} else if (parent == this) {
logger.warn(s"Parent of TypeSolver cannot be itself. setParent will be ignored.")
} else {
this.parent = parent
}
}
}

case class JavaFieldSummary(name: String, typeName: String) extends FieldLike derives ReadWriter {
def asJavaCode: List[String] = s"public $typeName $name;" :: Nil
}

case class JavaMethodSummary(
name: String,
returnType: String,
parameterTypes: List[(String, String)],
isStatic: Boolean
) extends MethodLike
derives ReadWriter {

def asJavaCode: List[String] =
s"""
|public${if (isStatic) " static" else ""} $returnType $name($parametersAsJavaCode){}
|""".stripMargin :: Nil

private def parametersAsJavaCode: String = parameterTypes.map { p => s"${p._2} ${p._1}" }.mkString(", ")
}

case class JavaTypeSummary(
name: String,
methods: List[JavaMethodSummary],
fields: List[JavaFieldSummary],
isStatic: Boolean,
innerClasses: List[JavaTypeSummary]
) extends TypeLike[JavaMethodSummary, JavaFieldSummary]
derives ReadWriter {

@targetName("add")
override def +(other: TypeLike[JavaMethodSummary, JavaFieldSummary]): TypeLike[JavaMethodSummary, JavaFieldSummary] =
this.copy(methods = mergeMethods(other), fields = mergeFields(other))

def asJavaCode: List[String] =
s"""
|public${if (isStatic) " static" else ""} class $name{
|${fields.flatMap(_.asJavaCode).mkString("\n")}
|${methods.flatMap(_.asJavaCode).mkString("\n")}
|${innerClasses.flatMap(_.asJavaCode).mkString("\n")}
|}""".stripMargin :: Nil
}

type NamespaceToTypeMap = Map[String, Set[JavaTypeSummary]]

class JavaProgramSummary(initialMappings: List[NamespaceToTypeMap] = List.empty)
extends ProgramSummary[JavaTypeSummary] {

override val namespaceToType: NamespaceToTypeMap = initialMappings.reduceOption(_ ++ _).getOrElse(Map.empty)

@targetName("add")
def ++(other: JavaProgramSummary): JavaProgramSummary = JavaProgramSummary(
ProgramSummary.combine(this.namespaceToType, other.namespaceToType) :: Nil
)

def asJavaCode: List[String] = namespaceToType.flatMap { (nsName, types) =>
types.map { typeSummary =>
s"""
|package $nsName;
|${typeSummary.asJavaCode.mkString}
|""".stripMargin
}
}.toList
}

object JavaProgramSummary {

private val logger = LoggerFactory.getLogger(getClass)

def empty: JavaProgramSummary = JavaProgramSummary(List.empty)

private def fromJsonInputStream(jsonInputStream: InputStream): Try[NamespaceToTypeMap] =
Try(read[NamespaceToTypeMap](ujson.Readable.fromByteArray(jsonInputStream.readAllBytes())))

def fromJsonString(jsonString: String): Try[NamespaceToTypeMap] =
fromJsonInputStream(ByteArrayInputStream(jsonString.getBytes))

def fromJsonDirectoryPath(path: String): List[NamespaceToTypeMap] = Try {
File(path)
.collectChildren(_.name.endsWith(".json"))
.flatMap { jsonFile =>
fromJsonInputStream(FileInputStream(jsonFile.toJava)) match {
case Success(mappings) => mappings :: Nil
case Failure(err) =>
logger.warn(s"Could not load Java type summary `${jsonFile.path}`:")
logger.warn(err.getMessage)
Nil
}
}
.toList
}.getOrElse(Nil)

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SourceParser(

private val logger = LoggerFactory.getLogger(this.getClass)

/** Parse the given file into a JavaParser CompliationUnit that will be used for creating the CPG AST.
/** Parse the given file into a JavaParser CompilationUnit that will be used for creating the CPG AST.
*
* @param relativeFilename
* path to the input file relative to the project root.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"okhttp3": [
{
"name": "OkHttpClient",
"methods": [
{
"name": "newCall",
"returnType": "Call",
"parameterTypes": [
[
"request",
"Request"
]
],
"isStatic": false
}
],
"fields": [],
"isStatic": false,
"innerClasses": [
{
"name": "Builder",
"methods": [
{
"name": "build",
"returnType": "OkHttpClient",
"parameterTypes": [],
"isStatic": false
}
],
"fields": [],
"isStatic": true,
"innerClasses": []
}
]
},
{
"name": "Request",
"methods": [],
"fields": [],
"isStatic": false,
"innerClasses": []
},
{
"name": "Call",
"methods": [
{
"name": "executeAsync",
"returnType": "void",
"parameterTypes": [],
"isStatic": false
}
],
"fields": [],
"isStatic": false,
"innerClasses": []
}
]
}
Loading

0 comments on commit 9c6da7c

Please sign in to comment.