Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.jacodb.api.jvm.JcClassExtFeature
import org.jacodb.api.jvm.JcClassOrInterface
import org.jacodb.api.jvm.JcClasspath
import org.jacodb.api.jvm.JcDatabase
import org.jacodb.api.jvm.JcDatabasePersistence
import org.jacodb.api.jvm.JcFeature
import org.jacodb.api.jvm.JcField
import org.jacodb.api.jvm.JcInstExtFeature
Expand All @@ -30,18 +31,17 @@ import org.jacodb.api.jvm.RegisteredLocation
import org.jacodb.api.jvm.cfg.JcInstList
import org.jacodb.api.jvm.cfg.JcRawInst
import org.jacodb.api.storage.StorageContext
import org.jacodb.api.storage.ers.Entity
import org.jacodb.api.storage.ers.EntityIterable
import org.jacodb.api.storage.ers.compressed
import org.jacodb.approximation.TransformerIntoVirtual.transformMethodIntoVirtual
import org.jacodb.approximation.annotation.Approximate
import org.jacodb.approximation.annotation.Version
import org.jacodb.impl.cfg.JcInstListImpl
import org.jacodb.impl.fs.className
import org.jacodb.impl.storage.dslContext
import org.jacodb.impl.storage.execute
import org.jacodb.impl.storage.jooq.tables.references.ANNOTATIONS
import org.jacodb.impl.storage.jooq.tables.references.ANNOTATIONVALUES
import org.jacodb.impl.storage.jooq.tables.references.CLASSES
import org.jacodb.impl.storage.txn
import org.jacodb.impl.types.RefKind
import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.ClassNode
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
Expand All @@ -59,11 +59,18 @@ import java.util.concurrent.ConcurrentMap
* otherwise you might get incomplete mapping.
* See [JcDatabase.awaitBackgroundJobs].
*/
object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeature {
class Approximations(
versions: List<VersionInfo>
) : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeature {

private val instSubstitutorForApproximations = InstSubstitutorForApproximations(this)
private val transformerIntoVirtual = TransformerIntoVirtual(this)

private val originalToApproximation: ConcurrentMap<OriginalClassName, ApproximationClassName> = ConcurrentHashMap()
private val approximationToOriginal: ConcurrentMap<ApproximationClassName, OriginalClassName> = ConcurrentHashMap()

private val versionMap: VersionMap = versions.associate { it.target to it.version }

override suspend fun query(classpath: JcClasspath, req: Any?): Sequence<Any?> {
// returns an empty sequence for now, all requests are made using
// findApproximationOrNull and findOriginalByApproximation functions
Expand All @@ -73,7 +80,29 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
override fun newIndexer(
jcdb: JcDatabase,
location: RegisteredLocation
): ByteCodeIndexer = ApproximationIndexer(originalToApproximation, approximationToOriginal)
): ByteCodeIndexer = ApproximationIndexer(originalToApproximation, approximationToOriginal, versionMap)

private val Entity.annotationNameId: Long?
get() = getCompressed<Long>("nameId")

private val Entity.annotationValueNameId: Long?
get() = getCompressedBlob<Long>("nameId")

private fun EntityIterable.annotationPrimitiveValueByName(valueNameId: Long): Long? {
val value = find { valueNameId == it.getCompressedBlob<Long>("nameId") }
return value?.getCompressedBlob<Long>("primitiveValue")
}

private fun EntityIterable.annotationStringValueByName(
persistence: JcDatabasePersistence,
valueNameId: Long
): String? {
val valueId = annotationPrimitiveValueByName(valueNameId)
if (valueId != null)
return persistence.findSymbolName(valueId)

return null
}

override fun onSignal(signal: JcSignal) {
if (signal is JcSignal.BeforeIndexing) {
Expand All @@ -82,24 +111,53 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
persistence.read { context ->
context.execute(
sqlAction = {
context.dslContext.select(CLASSES.NAME, ANNOTATIONVALUES.CLASS_SYMBOL)
.from(ANNOTATIONS)
.join(CLASSES).on(ANNOTATIONS.CLASS_ID.eq(CLASSES.ID))
.join(ANNOTATIONVALUES).on(ANNOTATIONVALUES.ANNOTATION_ID.eq(ANNOTATIONS.ID))
.where(
ANNOTATIONS.ANNOTATION_NAME.eq(approxSymbol).and(
ANNOTATIONVALUES.NAME.eq("value")
)
)
.fetch().asSequence().map { record -> record.value1() to record.value2() }
TODO("support versions for SQL persistence")
// context.dslContext.select(CLASSES.NAME, ANNOTATIONVALUES.CLASS_SYMBOL)
// .from(ANNOTATIONS)
// .join(CLASSES).on(ANNOTATIONS.CLASS_ID.eq(CLASSES.ID))
// .join(ANNOTATIONVALUES).on(ANNOTATIONVALUES.ANNOTATION_ID.eq(ANNOTATIONS.ID))
// .where(
// ANNOTATIONS.ANNOTATION_NAME.eq(approxSymbol).and(
// ANNOTATIONVALUES.NAME.eq("value")
// )
// )
// .fetch().asSequence().map { record -> record.value1() to record.value2() }
},
noSqlAction = {
val valueId = persistence.findSymbolId("value")
val versionsId = persistence.findSymbolId("versions")
val versionSymbol = persistence.findSymbolId(versionAnnotationClassName)
val targetId = persistence.findSymbolId("target")
val fromVersionId = persistence.findSymbolId("fromVersion")
val toVersionId = persistence.findSymbolId("toVersion")
context.txn.find("Annotation", "nameId", approxSymbol.compressed)
.filter { it.getCompressedBlob<Int>("refKind") == RefKind.CLASS.ordinal }
.flatMap { annotation ->
.mapNotNull { annotation ->
val values = annotation.getLinks("values")
val versionsValue = values.filterTo(mutableListOf()) {
versionsId == it.annotationValueNameId
}
if (versionsValue.isEmpty())
return@mapNotNull annotation to values

val versionMatches = versionsValue.any { versionValue ->
val versionAnnotation = versionValue.getLink("refAnnotation")
check(versionSymbol == versionAnnotation.annotationNameId)
val versionValues = versionAnnotation.getLinks("values")
val target = versionValues.annotationStringValueByName(persistence, targetId)
?: error("unable to find `target` value in `Version` annotation")
val fromVersion = versionValues.annotationStringValueByName(persistence, fromVersionId)
?: error("unable to find `fromVersion` value in `Version` annotation")
val toVersion = versionValues.annotationStringValueByName(persistence, toVersionId)
?: error("unable to find `toVersion` value in `Version` annotation")
VersionsIntervalInfo(target, fromVersion, toVersion).matches(versionMap)
}
if (versionMatches)
annotation to values
else null
}.flatMap { (annotation, values) ->
annotation.getLink("ref").let { clazz ->
annotation.getLinks("values").map { clazz to it }
values.map { clazz to it }
}
}.filter { (_, annotationValue) ->
valueId == annotationValue["nameId"]
Expand All @@ -125,13 +183,13 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
val approximationName = findApproximationByOriginOrNull(clazz.name.toOriginalName()) ?: return null
val approximationClass = clazz.classpath.findClassOrNull(approximationName) ?: return null

return approximationClass.declaredFields.map { TransformerIntoVirtual.transformIntoVirtualField(clazz, it) }
return approximationClass.declaredFields.map { transformerIntoVirtual.transformIntoVirtualField(clazz, it) }
}

/**
* Returns a list of [JcEnrichedVirtualMethod] if there is an approximation for [clazz] and null otherwise.
*/
override fun methodsOf(clazz: JcClassOrInterface): List<JcMethod>? {
override fun methodsOf(clazz: JcClassOrInterface): List<JcMethod>? = with(transformerIntoVirtual) {
val approximationName = findApproximationByOriginOrNull(clazz.name.toOriginalName()) ?: return null
val approximationClass = clazz.classpath.findClassOrNull(approximationName) ?: return null

Expand All @@ -141,7 +199,7 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
}

override fun transformRawInstList(method: JcMethod, list: JcInstList<JcRawInst>): JcInstList<JcRawInst> {
return JcInstListImpl(list.map { it.accept(InstSubstitutorForApproximations) })
return JcInstListImpl(list.map { it.accept(instSubstitutorForApproximations) })
}

/**
Expand All @@ -159,10 +217,118 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
): String? = approximationToOriginal[className]?.className
}

data class VersionInfo(
val target: String,
val version: String,
) {
init {
check(version.isVersion)
}
}

private typealias VersionMap = Map<String, String>

private data class VersionsIntervalInfo(
val target: String,
val fromVersion: String,
val toVersion: String,
) {
init {
check(fromVersion.isVersion)
check(toVersion.isVersion)
}

fun matches(versionMap: VersionMap): Boolean {
check(fromVersion.isVersion && toVersion.isVersion)

val version = versionMap[this.target] ?: return false

val fromVersionNumbers = fromVersion.toNumbers
val toVersionNumbers = toVersion.toNumbers
val versionNumbers = version.toNumbers

return versionNumbers.isVersionInRange(fromVersionNumbers, toVersionNumbers)
}
}

/**
* Checks if the string is a version in the form of 3 numbers separated by dots, e.g., "1.2.3".
*/
private val String.isVersion: Boolean get() =
Regex("^(\\d+)\\.(\\d+)\\.(\\d+)$").matches(this)

private val String.toNumbers: IntArray get() {
val numbers = this.split('.')
check(numbers.size == 3)
return IntArray(numbers.size) { i -> numbers[i].toInt() }
}

/**
* Checks if a version (array of 3 numbers) is within the inclusive range defined by fromVersion and toVersion (also arrays of 3 numbers).
* Example: 1.1.1 <= 1.2.0 <= 2.0.0
*/
private fun IntArray.isVersionInRange(fromVersion: IntArray, toVersion: IntArray): Boolean {
require(this.size == 3 && fromVersion.size == 3 && toVersion.size == 3) { "All version arrays must have size 3" }
for (i in 0..2) {
if (this[i] < fromVersion[i]) return false
if (this[i] > fromVersion[i]) break
}
for (i in 0..2) {
if (this[i] > toVersion[i]) return false
if (this[i] < toVersion[i]) break
}
return true
}

private class ApproximationIndexer(
private val originalToApproximation: ConcurrentMap<OriginalClassName, ApproximationClassName>,
private val approximationToOriginal: ConcurrentMap<ApproximationClassName, OriginalClassName>
private val approximationToOriginal: ConcurrentMap<ApproximationClassName, OriginalClassName>,
private val versionMap: VersionMap
) : ByteCodeIndexer {

private fun annotationValueByName(versionValues: List<Any>, name: String): Any? {
val valueNameIdx = versionValues.indexOf(name)
if (valueNameIdx == -1)
return null

val valueIdx = valueNameIdx + 1
if (valueIdx >= versionValues.size)
return null

return versionValues[valueIdx]
}

private fun annotationStringValueByName(versionValues: List<Any>, name: String): String? {
return annotationValueByName(versionValues, name) as? String
}

private val AnnotationNode.versionIntervalInfo: VersionsIntervalInfo get() {
val target = annotationStringValueByName(values, "target")
?: error("unable to find `target` value in `Version` annotation")
val from = annotationStringValueByName(values, "fromVersion")
?: error("unable to find `target` value in `Version` annotation")
val to = annotationStringValueByName(values, "toVersion")
?: error("unable to find `target` value in `Version` annotation")
return VersionsIntervalInfo(target, from, to)
}

private fun checkVersion(approximationAnnotation: AnnotationNode): Boolean {
val versions = annotationValueByName(approximationAnnotation.values, "versions") as? List<*>
// When `Approximate` annotation does not contain `Version` annotation, it matches any version
?: return true

if (versions.isEmpty())
return true

for (version in versions) {
version as AnnotationNode
if (version.versionIntervalInfo.matches(versionMap))
return true
}

return false
}

override fun index(classNode: ClassNode) {
val annotations = classNode.visibleAnnotations ?: return

Expand All @@ -171,6 +337,9 @@ private class ApproximationIndexer(
approximationAnnotationClassName in it.desc.className
} ?: return

if (!checkVersion(approximationAnnotation))
return

// Extract a name of the target class for this approximation
val target = approximationAnnotation.values.filterIsInstance<org.objectweb.asm.Type>().single()

Expand Down Expand Up @@ -201,6 +370,7 @@ private class ApproximationIndexer(
}

private val approximationAnnotationClassName = Approximate::class.qualifiedName!!
private val versionAnnotationClassName = Version::class.qualifiedName!!

@JvmInline
value class ApproximationClassName(val className: String) {
Expand Down
Loading