Skip to content

Commit

Permalink
Merge pull request #36 from sse-labs/feature/modular-ifds
Browse files Browse the repository at this point in the history
Merge development progress of modular analysis
  • Loading branch information
johannesduesing authored Nov 14, 2024
2 parents 89c233a + 13bec0c commit 2220a7d
Show file tree
Hide file tree
Showing 113 changed files with 5,015 additions and 778 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/sbt-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: SBT Unit Tests

on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
cache: 'sbt'
- name: Run tests
run: sbt test
2 changes: 0 additions & 2 deletions analysis-runner/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@ spa-reuse {
exec-pool-size=1

exit-on-enter=true

jre-data-dir="jre-data"
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ class MvnConstantClassAnalysisImpl extends AnalysisImplementation {
case _ => throw new IllegalArgumentException("Corrupt input entity hierarchy")
}

val classToHashesMap = allClasses.map { jc => (jc.identifier, mutable.ListBuffer.empty[String]) }.toMap
val classToHashesMap = allClasses.map { jc => (jc.thisType, mutable.ListBuffer.empty[String]) }.toMap

allClasses.foreach(jc => classToHashesMap(jc.identifier).append(jc.binaryHash.map(toHex).getOrElse{
log.warn(s"Class without hash: ${jc.identifier}")
allClasses.foreach(jc => classToHashesMap(jc.thisType).append(jc.binaryHash.map(toHex).getOrElse{
log.warn(s"Class without hash: ${jc.name}")
""
}))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package org.anon.spareuse.execution.analyses.impl

import org.anon.spareuse.core.formats
import org.anon.spareuse.core.formats.{ListResultFormat, NamedPropertyFormat, ObjectResultFormat}
import org.anon.spareuse.core.model.SoftwareEntityKind.SoftwareEntityKind
import org.anon.spareuse.core.model.entities.JavaEntities.JavaLibrary
import org.anon.spareuse.core.model.entities.SoftwareEntityData
import org.anon.spareuse.core.model.{AnalysisData, SoftwareEntityKind}
import org.anon.spareuse.core.utils.{SemVer, compareSemanticVersions, parseSemVer}
import org.anon.spareuse.execution.analyses.impl.MvnConstantMethodsAnalysisImpl.{LibraryResult, MethodIdent, ReleaseResult}
import org.anon.spareuse.execution.analyses.{AnalysisImplementation, AnalysisImplementationDescriptor, AnalysisResult, FreshResult}

import scala.collection.mutable
import scala.util.{Success, Try}

class MvnConstantMethodsAnalysisImpl extends AnalysisImplementation {
override val descriptor: AnalysisImplementationDescriptor = MvnConstantMethodsAnalysisImpl

override def executionPossible(inputs: Seq[SoftwareEntityData], rawConfig: String): Boolean = {
if (inputs.exists(e => !e.isInstanceOf[JavaLibrary])) {
log.warn(s"Execution of analysis ${descriptor.fullName} not possible: Inputs must be of kind 'Library'")
false
} else {
true
}
}

override def executeAnalysis(inputs: Seq[SoftwareEntityData], rawConfig: String): Try[Set[AnalysisResult]] = {
if (!rawConfig.isBlank)
log.warn("Non-Empty configuration will be ignored, this analysis does not support configuration options")

Try { inputs.map(input => buildResult(input.asInstanceOf[JavaLibrary]).get).toSet }
}

private def buildResult(lib: JavaLibrary): Try[AnalysisResult] = Try {
val releases = lib.getPrograms

val numReleases = releases.size
val avgMethodsPerRelease = releases.map(_.allMethods.size).sum.toDouble / numReleases

val totalMethodVersionsSeen = mutable.Map.empty[MethodIdent, mutable.Set[Int]]

val sortedReleases = releases.toSeq.sortWith{ (r1, r2) =>
Try(compareSemanticVersions(r1.v, r2.v)) match {
case Success(compResult) => compResult < 0
case _ => r1.v.compareTo(r2.v) < 0
}
}

var lastReleaseMethods: Option[Map[MethodIdent, Int]] = None
var lastReleaseVersion: Option[SemVer] = None

val releaseInfo = sortedReleases.map { currentRelease =>
val currentReleaseMethods = currentRelease
.allMethods
.map(jm => MethodIdent(jm.enclosingClass.get.thisType, jm.name, jm.descriptor) -> jm.methodHash)
.toMap

val totalMethods = currentReleaseMethods.size

var overallNewMethodsSeen = 0
var updateNewMethodsSeen = 0

currentRelease
.allMethods
.foreach{ currMethod =>
val currIdent = MethodIdent(currMethod.enclosingClass.get.thisType, currMethod.name, currMethod.descriptor)

if(!totalMethodVersionsSeen.contains(currIdent)){
overallNewMethodsSeen += 1
totalMethodVersionsSeen.put(currIdent, mutable.Set(currMethod.methodHash))
} else if(!totalMethodVersionsSeen(currIdent).contains(currMethod.methodHash)){
overallNewMethodsSeen += 1
totalMethodVersionsSeen(currIdent).add(currMethod.methodHash)
}

if(lastReleaseMethods.isDefined){
val last = lastReleaseMethods.get
if(!last.contains(currIdent) || last(currIdent) != currMethod.methodHash) updateNewMethodsSeen += 1
} else {
updateNewMethodsSeen += 1
}

}

lastReleaseMethods = Some(currentReleaseMethods)

val currentVersion = parseSemVer(currentRelease.v).toOption
val updateType = if(currentVersion.isDefined && lastReleaseVersion.isDefined)
currentVersion.get.getUpdateType(lastReleaseVersion.get) else "NO_SEM_VER"

lastReleaseVersion = currentVersion

ReleaseResult(currentRelease.v, totalMethods, overallNewMethodsSeen, updateNewMethodsSeen, updateType)
}.toList

val theResult = LibraryResult(lib.libraryName, numReleases, avgMethodsPerRelease, releaseInfo)

FreshResult(theResult, Set(lib))
}

}

object MvnConstantMethodsAnalysisImpl extends AnalysisImplementationDescriptor {

private val releaseInfoFormat = ObjectResultFormat(
NamedPropertyFormat("version", formats.StringFormat),
NamedPropertyFormat("totalMethods", formats.NumberFormat),
NamedPropertyFormat("overallNewMethods", formats.NumberFormat),
NamedPropertyFormat("updateNewMethods", formats.NumberFormat),
NamedPropertyFormat("updateType", formats.StringFormat)
)

private val resultFormat = ObjectResultFormat(
NamedPropertyFormat("ga", formats.StringFormat, "GA of this library"),
NamedPropertyFormat("numReleases", formats.NumberFormat, "Number of library releases"),
NamedPropertyFormat("avgMethodsPerRelease", formats.NumberFormat, "Number of average methods per release"),
NamedPropertyFormat("releaseInfo", ListResultFormat(releaseInfoFormat), "Statistics on each release")
)

override val analysisData: AnalysisData = AnalysisData.systemAnalysis(
name = "mvn-constant-methods",
version = "1.0.0",
description = "Processes Maven library (GA-Tuple) and computes basic statistics on how often methods actually change on updates",
builtOn = "built-in facilities",
Set("java", "scala"),
resultFormat,
SoftwareEntityKind.Library,
doesBatchProcessing = true,
isIncremental = false
)


override val requiredInputResolutionLevel: SoftwareEntityKind = SoftwareEntityKind.Method

private case class MethodIdent(classFqn: String, methodName: String, descriptor: String) {
override def toString: String = s"$classFqn.$methodName : $descriptor"

override def hashCode(): Int = classFqn.hashCode + 11*methodName.hashCode + 17*descriptor.hashCode
}

case class LibraryResult(ga: String, numReleases: Int, avgMethodsPerRelease: Double, releaseInfo: List[ReleaseResult])
case class ReleaseResult(version: String, totalMethods: Int, overallNewMethods: Int, updateNewMethods: Int, updateType: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ class MvnPartialCallgraphAnalysisImpl extends AnalysisImplementation {
Some(FreshResult(resultContent, Set(program)))

case Failure(HttpDownloadException(status, _, _)) if status == 404 =>
log.warn(s"No JAR file available for ${program.identifier}")
log.warn(s"No JAR file available for ${program.name}")
None
case Failure(ex) =>
log.error(s"Failed to download JAR file contents for ${program.identifier}", ex)
log.error(s"Failed to download JAR file contents for ${program.name}", ex)
throw ex

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.anon.spareuse.execution.analyses.impl.cg

import org.anon.spareuse.core.model.entities.JavaEntities.{JavaClass, JavaInvocationType, JavaInvokeStatement, JavaMethod, JavaProgram}
import org.anon.spareuse.execution.analyses.impl.cg.AbstractRTABuilder.TypeNode
import org.anon.spareuse.execution.analyses.impl.cg.CallGraphBuilder.DefinedMethod
import org.opalj.br.FieldType
import org.slf4j.{Logger, LoggerFactory}

Expand Down Expand Up @@ -48,7 +50,7 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
.flatMap { version =>
if (JreModelLoader.hasJre(version)) tryToOpt(JreModelLoader.getJre(version))
else {
log.warn(s"Requested JRE version $version to available, using default.")
log.warn(s"Requested JRE version $version not available, using default.")
tryToOpt(JreModelLoader.getDefaultJre)
}
}
Expand All @@ -58,7 +60,7 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
* Builds the type hierarchy based on all inputs and the JRE version currently specified.
* @return A Map of type FQNs to their TypeNodes, which hold the parent / child relation
*/
private[cg] def buildTypeHierarchy(): Map[String, TypeNode] = {
protected[cg] def buildTypeHierarchy(): Map[String, TypeNode] = {

val jreTypeMap = if (jreOpt.isDefined) {
jreOpt.get.types.map { jreType =>
Expand Down Expand Up @@ -142,20 +144,24 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
/**
* Retrieve all children of the given type node that are instantiated somewhere
* @param typeNode TypeNode to enumerate children for
* @param instantiatedTypes Set of instantiated type names
* @param typeSelectable Function determining whether a type is selectable as a target
* @return Set of TypeNodes that are children of the given node and instantiated somewhere
*/
protected[cg]def getPossibleChildNodes(typeNode: TypeNode, instantiatedTypes: Set[String]): Set[TypeNode] = {
typeNode.allChildren.filter(cNode => instantiatedTypes.contains(cNode.thisType))
protected[cg]def getPossibleChildNodes(typeNode: TypeNode, typeSelectable: String => Boolean): Set[TypeNode] = {
typeNode.allChildren.filter(cNode => typeSelectable(cNode.thisType))
}

private[cg] def resolveOnObject(jis: JavaInvokeStatement, instantiatedTypes: Set[String]): Set[DefinedMethod] =
instantiatedTypes
.flatMap(t => classLookup(t).lookupMethod(jis.targetMethodName, jis.targetDescriptor).map(asDefinedMethod)) ++
private[cg] def resolveOnObject(jis: JavaInvokeStatement, typeSelectable: String => Boolean): Set[DefinedMethod] = {
typeLookup
.values
.filter(n => typeSelectable(n.thisType))
.flatMap(n => findMethodOn(jis, n))
.toSet ++
getMethodOnObjectType(jis).toSet
}


protected[cg] def resolveInvocation(jis: JavaInvokeStatement, declType: TypeNode, instantiatedTypes: Set[String]): Set[DefinedMethod] = jis.invokeStatementType match {
protected[cg] def resolveInvocation(jis: JavaInvokeStatement, declType: TypeNode, callingContext: DefinedMethod, typeSelectable: String => Boolean): Set[DefinedMethod] = jis.invokeStatementType match {
case JavaInvocationType.Static =>
// Static methods only need to be looked for at the precise declared type!
var targetOpt = findMethodOn(jis, declType)
Expand All @@ -171,9 +177,9 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
targetOpt.toSet

case JavaInvocationType.Virtual | JavaInvocationType.Interface =>
if(declType.thisType == "java/lang/Object") return resolveOnObject(jis, instantiatedTypes)
if(declType.thisType == "java/lang/Object") return resolveOnObject(jis, typeSelectable)

val targets = getPossibleChildNodes(declType, instantiatedTypes)
val targets = getPossibleChildNodes(declType, typeSelectable)
.flatMap(node => findMethodOn(jis, node))

// "Easy" approximation: Always consider base definition if available. Technically it might not be reachable if the
Expand Down Expand Up @@ -209,14 +215,13 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
/**
* Resolves the given invocation with the given set of instantiatable types
* @param jis Invocation statment to resolve
* @param instantiatedTypes Set of type names that may be instantiated at this point in time
* @param callingContext Method this call was made from
* @param typeSelectable Function determining whether a type is selectable as a target (usually: Is it instantiatable?)
* @param simpleCaching Set to true in order to cache results only per invocation, not considering types. Only applicable
* if the types considered instantiatable are always the same (ie. in naive RTA).
* @return Set of defined methods that may be invoked
*/
def resolveInvocation(jis: JavaInvokeStatement, instantiatedTypes: Set[String], simpleCaching: Boolean = false): Set[DefinedMethod] = {


def resolveInvocation(jis: JavaInvokeStatement, callingContext: DefinedMethod, typeSelectable: String => Boolean, simpleCaching: Boolean = false): Set[DefinedMethod] = {

if(simpleCaching ){
val invocationIdent = jis.targetTypeName + jis.targetMethodName + jis.targetDescriptor
Expand All @@ -231,7 +236,7 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
Set.empty
} else {
val declaredType = typeLookup(objTypeFqn)
resolveInvocation(jis, declaredType, instantiatedTypes)
resolveInvocation(jis, declaredType, callingContext, typeSelectable)
}
case '<' =>
log.warn(s"Resolution for INVOKEDYNAMIC not supported: $jis")
Expand All @@ -250,12 +255,12 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
if(at.isObjectType){
// hashCode, equals and toString on array types involve calling the respective method on their component type!
// Hence, we rewrite the invoke statement to point to the component type instead
val correctedInvocation = new JavaInvokeStatement(jis.targetMethodName, at.toJVMTypeName, jis.targetDescriptor, jis.invokeStatementType, jis.instructionPc, jis.uid, jis.repository)
resolveInvocation(correctedInvocation, instantiatedTypes)
val correctedInvocation = new JavaInvokeStatement(jis.targetMethodName, at.toJVMTypeName, jis.targetDescriptor, jis.invokeStatementType, jis.instructionPc, jis.id, jis.repository)
resolveInvocation(correctedInvocation, callingContext, typeSelectable, simpleCaching)
} else {
// if one of the three methods is invoked on a primitive component type, we refer to object as the declaring type
val correctedInvocation = new JavaInvokeStatement(jis.targetMethodName, "java/lang/Object", jis.targetDescriptor, jis.invokeStatementType, jis.instructionPc, jis.uid, jis.repository)
resolveInvocation(correctedInvocation, instantiatedTypes)
val correctedInvocation = new JavaInvokeStatement(jis.targetMethodName, "java/lang/Object", jis.targetDescriptor, jis.invokeStatementType, jis.instructionPc, jis.id, jis.repository)
resolveInvocation(correctedInvocation, callingContext, typeSelectable, simpleCaching)
}
case _ =>
log.warn(s"Unable to resolve method invocation on array type: ${jis.targetTypeName}->${jis.targetMethodName}")
Expand Down Expand Up @@ -288,7 +293,12 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:

protected def buildTypeNode(jt: JreType): TypeNode = new TypeNode(jt.t, jt.s, jt.i.toSet, jt.iI)

protected[cg] class TypeNode(thisTypeFqn: String, superTypeFqnOpt: Option[String], interfaceTypeFqns: Set[String], isInterfaceNode: Boolean) {


}

object AbstractRTABuilder {
class TypeNode(thisTypeFqn: String, superTypeFqnOpt: Option[String], interfaceTypeFqns: Set[String], isInterfaceNode: Boolean) {

private var parent: Option[TypeNode] = None
private val interfaces: mutable.Set[TypeNode] = mutable.Set.empty
Expand All @@ -304,6 +314,10 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
getChildren.flatMap(c => Set(c) ++ c.allChildren)
}

lazy val allParents: Seq[TypeNode] = {
getParent.map(p => Seq(p) ++ p.allParents).getOrElse(Seq.empty)
}

def setParent(p: TypeNode): Unit = {
parent = Some(p)
p.children.add(this)
Expand All @@ -314,9 +328,6 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
def getParent: Option[TypeNode] = parent

def addImplements(t: TypeNode): Unit = {
if (!t.isInterface)
log.warn(s"${t.thisType} is not an interface type, yet it is implemented by $thisType")

interfaces.add(t)
t.children.add(this)
}
Expand All @@ -330,5 +341,4 @@ abstract class AbstractRTABuilder(programs: Set[JavaProgram], jreVersionToLoad:
def isIncomplete: Boolean = superTypeOpt.isDefined && parent.isEmpty

}

}
Loading

0 comments on commit 2220a7d

Please sign in to comment.