fix(indexing): split indexing logic into 'api' and 'core' modules

This commit is contained in:
Akash Yadav
2024-06-13 17:39:44 +05:30
parent ea46d6a1c2
commit d98aaa9c03
52 changed files with 1037 additions and 409 deletions
+6 -3
View File
@@ -46,21 +46,24 @@ kapt {
dependencies {
kapt(projects.annotation.processors)
kapt(libs.google.auto.service)
implementation(libs.androidide.ts)
implementation(libs.androidide.ts.java)
implementation(libs.androidx.annotation)
implementation(libs.androidx.appcompat)
implementation(libs.common.editor)
implementation(libs.common.javaparser)
implementation(libs.common.utilcode)
implementation(libs.androidx.annotation)
implementation(libs.google.auto.service.annotations)
implementation(libs.google.guava)
implementation(libs.google.gson)
implementation(libs.google.material)
implementation(projects.core.actions)
implementation(projects.core.common)
implementation(projects.core.indexing)
implementation(projects.core.indexingApi)
implementation(projects.core.lspApi)
implementation(projects.core.resources)
implementation(projects.editor.api)
@@ -80,4 +83,4 @@ dependencies {
androidTestImplementation(projects.testing.androidTest)
androidTestImplementation(projects.utilities.shared)
}
}
@@ -20,7 +20,6 @@ package com.itsaky.androidide.lsp.java.indexing
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaClass
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
import io.realm.RealmList
@@ -22,7 +22,6 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.blankj.utilcode.util.ConvertUtils
import com.google.common.truth.Truth
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
import io.realm.RealmModel
import org.junit.Rule
@@ -19,13 +19,12 @@ package com.itsaky.androidide.lsp.java.indexing
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import com.itsaky.androidide.indexing.core.platform.ApiInfo
import com.itsaky.androidide.lsp.java.indexing.classfile.ArrayAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.ClassAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.EnumAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.IAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaConstant
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
import com.itsaky.androidide.lsp.java.indexing.classfile.PrimitiveAnnotationElementValue
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
@@ -49,7 +48,7 @@ class SharedModelCRUDTest {
@Test
fun testApiInfoCreate() {
dbTestRule.withDb("api-info") {
val apiInfo = ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23)
val apiInfo = com.itsaky.androidide.indexing.core.platform.ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23)
assertInsertUnique(apiInfo)
val apiInfo2 = apiInfo.clone().update(removedIn = 26)
@@ -63,7 +62,7 @@ class SharedModelCRUDTest {
@Test(expected = RealmPrimaryKeyConstraintException::class)
fun testApiInfoDuplicationError() {
dbTestRule.withDb("api-info-duplication-error") {
val apiInfo = ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23)
val apiInfo = com.itsaky.androidide.indexing.core.platform.ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23)
assertInsertSingle(apiInfo)
assertInsertSingle(apiInfo)
}
@@ -17,8 +17,6 @@
package com.itsaky.androidide.lsp.java.indexing
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
/**
* A java symbol.
*
@@ -28,5 +26,4 @@ import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
*/
interface IJavaSymbol : IJavaIndexable {
var accessFlags: Int
var apiInfo: ApiInfo?
}
@@ -1,56 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.java.indexing
import com.itsaky.androidide.indexing.IIndex
import com.itsaky.androidide.indexing.IIndexer
import com.itsaky.androidide.indexing.IIndexer.Params
import com.itsaky.androidide.tasks.cancelIfActive
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import java.io.File
/**
* @author Akash Yadav
*/
class JavaIndexer(
private val jarPaths: List<File>
) : IIndexer<IJavaIndexable, Unit> {
companion object {
const val INDEX_PATH = "/java"
}
private val coroutineScope = CoroutineScope(Dispatchers.IO)
override val indexPath: String = INDEX_PATH
override fun index(index: IIndex<IJavaIndexable>, params: Params): Collection<Deferred<Unit>> {
return jarPaths.map { jar ->
coroutineScope.async {
val worker = JavaJarModelBuilder(jar)
// worker.index(index)
}
}
}
override fun close() {
coroutineScope.cancelIfActive("Cancellation requested")
}
}
@@ -15,9 +15,22 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.java.indexing.classfile
package com.itsaky.androidide.lsp.java.indexing
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import com.itsaky.androidide.lsp.java.indexing.classfile.AnnotationAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.AnnotationElement
import com.itsaky.androidide.lsp.java.indexing.classfile.ArrayAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.ClassAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.EnumAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaAnnotation
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaClass
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaConstant
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaEnum
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaField
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaInterface
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaMethod
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
import com.itsaky.androidide.lsp.java.indexing.classfile.PrimitiveAnnotationElementValue
import io.realm.annotations.RealmModule
/**
@@ -33,7 +46,6 @@ import io.realm.annotations.RealmModule
ClassAnnotationElementValue::class,
EnumAnnotationElementValue::class,
PrimitiveAnnotationElementValue::class,
ApiInfo::class,
JavaAnnotation::class,
JavaClass::class,
JavaConstant::class,
@@ -269,17 +269,18 @@ class JavaJarModelBuilder(private val jar: File) {
val methodName = method.getName(classFile.constant_pool)
val methodType = DescriptorUtils.returnType(descriptor)
add(this@setAnnotationElements.newAnnotationElement(
methodName, methodType, method.access_flags.flags
).apply {
val annotationDefault =
method.attributes.get(Attribute.AnnotationDefault) as? AnnotationDefault_attribute?
if (annotationDefault != null) {
val defaultValue = annotationDefault.default_value
val value = toAnnotationElementValue(defaultValue, classFile)
this.defaultValue = value.let(RealmAny::valueOf) ?: RealmAny.nullValue()
}
})
add(
newAnnotationElement(
methodName, methodType, method.access_flags.flags
).apply {
val annotationDefault =
method.attributes.get(Attribute.AnnotationDefault) as? AnnotationDefault_attribute?
if (annotationDefault != null) {
val defaultValue = annotationDefault.default_value
val value = toAnnotationElementValue(defaultValue, classFile)
this.defaultValue = value.let(RealmAny::valueOf) ?: RealmAny.nullValue()
}
})
}
}
}
@@ -1,122 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.java.indexing.apiinfo
import com.google.common.base.Objects
import com.itsaky.androidide.lsp.java.indexing.ICloneable
import com.itsaky.androidide.lsp.java.indexing.ISharedJavaIndexable
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* @property since The API in which the symbol was added.
* @property deprecatedIn The API in which the symbol was deprecated.
* @property removedIn The API in which the symbol was removed.
* @author Akash Yadav
*/
@RealmClass
open class ApiInfo : ISharedJavaIndexable, ICloneable {
@Required
@PrimaryKey
@RealmField("id")
override var id: Int? = null
@RealmField("since")
var since: Int = 1
private set
@RealmField("deprecatedIn")
var deprecatedIn: Int = 0
private set
@RealmField("removedIn")
var removedIn: Int = 0
private set
override fun computeId() {
this.id = Objects.hashCode(this.since, this.removedIn, this.deprecatedIn)
}
/**
* Update the API information from the given [ApiInfo].
*/
fun update(apiInfo: ApiInfo) = apply {
this.since = apiInfo.since
this.deprecatedIn = apiInfo.deprecatedIn
this.removedIn = apiInfo.removedIn
this.computeId()
}
/**
* Update the API information with the given parameters.
*/
fun update(
since: Int = this.since,
deprecatedIn: Int = this.deprecatedIn,
removedIn: Int = this.removedIn
) = apply {
this.since = since
this.deprecatedIn = deprecatedIn
this.removedIn = removedIn
this.computeId()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ApiInfo) return false
if (id != other.id) return false
if (since != other.since) return false
if (deprecatedIn != other.deprecatedIn) return false
if (removedIn != other.removedIn) return false
return true
}
override fun hashCode(): Int {
var result = id ?: 0
result = 31 * result + since
result = 31 * result + deprecatedIn
result = 31 * result + removedIn
return result
}
override fun toString(): String {
return "ApiInfo(id=$id, since=$since, deprecatedIn=$deprecatedIn, removedIn=$removedIn)"
}
override fun clone(): ApiInfo {
return newInstance(since, deprecatedIn, removedIn)
}
companion object {
@JvmStatic
fun newInstance(since: Int, deprecatedIn: Int, removedIn: Int): ApiInfo {
return ApiInfo().apply {
this.since = since
this.deprecatedIn = deprecatedIn
this.removedIn = removedIn
this.computeId()
}
}
}
}
@@ -1,184 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.java.indexing.apiinfo
import androidx.collection.mutableIntObjectMapOf
import jaxp.xml.namespace.QName
import jaxp.xml.stream.XMLInputFactory
import jaxp.xml.stream.events.Attribute
import jaxp.xml.stream.events.EndElement
import jaxp.xml.stream.events.StartElement
import org.slf4j.LoggerFactory
import java.io.InputStream
/**
* Parser for parsing `api-versions.xml` file from the Android SDK and building [ApiInfo] models.
*
* @author Akash Yadav
*/
class ApiInfoParser {
private val apiInfos = mutableIntObjectMapOf<ApiInfo>()
private val apiInfo = HashMap<String, Pair<ApiInfo, HashMap<String, ApiInfo>>>()
private var apiVersion: Int? = null
private var currentClass: String? = null
companion object {
private val log = LoggerFactory.getLogger(ApiInfoParser::class.java)
}
fun parse(apiVersionsXml: InputStream) {
if (apiVersionsXml.available() <= 0) {
log.warn("api-versions.xml InputStream is empty")
return
}
val inputFactory = XMLInputFactory.newInstance()
val reader = inputFactory.createXMLEventReader(apiVersionsXml)
while (reader.hasNext()) {
val event = reader.nextEvent()
if (event.isStartElement) {
consumeStartElement(event.asStartElement())
continue
}
if (event.isEndElement) {
consumeEndElement(event.asEndElement())
}
}
}
/**
* Returns [ApiInfo] for the given class.
*/
fun getClassInfo(className: String): ApiInfo? {
return apiInfo[className]?.first
}
/**
* Returns [ApiInfo] for the given class and member.
*/
fun getMemberInfo(className: String, memberName: String): ApiInfo? {
return apiInfo[className]?.second?.get(memberName)
}
/**
* Removes and returns the [ApiInfo] for the given class and member. This also removes the [ApiInfo]
* for the class if the all the members of the class have been removed.
*/
fun removeMemberInfo(className: String, memberName: String): ApiInfo? {
return apiInfo[className]?.second?.remove(memberName).also {
if (apiInfo[className]?.second?.isEmpty() == true) {
apiInfo.remove(className)
}
}
}
private fun consumeStartElement(event: StartElement) {
when (event.name.localPart) {
"api" -> apiVersion = event.getAttributeByName(QName("version")).value.toInt()
"class" -> consumeClass(event)
"field" -> consumeMember(event, "field")
"method" -> consumeMember(event, "method")
}
}
private fun consumeClass(event: StartElement) {
checkNotNull(apiVersion) {
"<class> element must be inside <api> element"
}
check(currentClass == null) {
"<class> elements cannot be nested"
}
val (name, versions) = event.parseAttrs()
check(!apiInfo.containsKey(name)) {
"Duplicate class entry: $name"
}
val apiInfo = apiInfos.getOrPut(versions) {
createApiInfo(versions)
}
this.apiInfo[name] = apiInfo to HashMap()
this.currentClass = name
}
private fun consumeMember(event: StartElement, memberType: String) {
val (name, versions) = event.parseAttrs()
val currentClass = checkNotNull(currentClass) {
"<${memberType}> element must be inside <class> element"
}
check(!apiInfo.containsKey(name)) {
"Duplicate $memberType entry in class $currentClass: $name"
}
val (_, members) = apiInfo[currentClass]!!
val existing = members.put(name, createApiInfo(versions))
check(existing == null) {
"Duplicate $memberType entry in class $currentClass: $name"
}
}
private fun consumeEndElement(element: EndElement) {
when (element.name.localPart) {
"api" -> apiVersion = null
"class" -> currentClass = null
}
}
private fun StartElement.parseAttrs(): Pair<String, Int> {
var name: String? = null
var since = 1
var deprecated = 0
var removed = 0
attributes.forEach { attribute ->
attribute as Attribute
when (attribute.name.localPart) {
"name" -> name = attribute.value
"since" -> since = attribute.value.toInt()
"deprecated" -> deprecated = attribute.value.toInt()
"removed" -> removed = attribute.value.toInt()
}
}
checkNotNull(name) {
"Missing name attribute"
}
// Android API versions would not exceed 255, right?
check(since in 1..255 && deprecated in 0..255 && removed in 0..255) {
"Invalid version: $since, $deprecated, $removed"
}
val version = (since shl 16) + (deprecated shl 8) + removed
return name!! to version
}
private fun createApiInfo(versions: Int): ApiInfo {
val since = (versions shr 16) and 0x000000FF
val deprecated = (versions shr 8) and 0x000000FF
val removed = versions and 0x000000FF
return ApiInfo.newInstance(since = since, deprecatedIn = deprecated, removedIn = removed)
}
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.itsaky.androidide.lsp.java.indexing.IJavaSymbol
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import io.realm.RealmAny
import io.realm.RealmResults
import io.realm.annotations.Index
@@ -47,9 +46,6 @@ open class AnnotationElement : IJavaSymbol {
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@LinkingObjects("methods")
val ofClass: RealmResults<JavaAnnotation>? = null
@@ -78,18 +74,16 @@ open class AnnotationElement : IJavaSymbol {
companion object {
@JvmStatic
fun JavaAnnotation.newAnnotationElement(
fun newAnnotationElement(
name: String,
type: JavaType,
accessFlags: Int,
apiInfo: ApiInfo? = null,
defaultValue: RealmAny? = null
): AnnotationElement {
return AnnotationElement().apply {
this.name = name
this.type = type
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.defaultValue = defaultValue
}
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.itsaky.androidide.lsp.java.indexing.IJavaType
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import io.realm.RealmList
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
@@ -66,9 +65,6 @@ open class JavaAnnotation : IJavaType<JavaField, AnnotationElement> {
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("fields")
override var fields: RealmList<JavaField>? = null
@@ -90,7 +86,6 @@ open class JavaAnnotation : IJavaType<JavaField, AnnotationElement> {
accessFlags: Int,
isInner: Boolean = false,
superInterfacesFqn: RealmList<String>? = null,
apiInfo: ApiInfo? = null,
fields: RealmList<JavaField>? = null,
methods: RealmList<AnnotationElement>? = null
): JavaAnnotation {
@@ -102,7 +97,6 @@ open class JavaAnnotation : IJavaType<JavaField, AnnotationElement> {
this.superClassFqn = superClassFqn
this.superInterfacesFqn = superInterfacesFqn
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.fields = fields
this.methods = methods
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.itsaky.androidide.lsp.java.indexing.IJavaType
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import io.realm.RealmList
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
@@ -61,9 +60,6 @@ open class JavaClass : IJavaType<JavaField, JavaMethod> {
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("fields")
override var fields: RealmList<JavaField>? = null
@@ -83,7 +79,6 @@ open class JavaClass : IJavaType<JavaField, JavaMethod> {
isInner: Boolean = false,
superClassFqn: String? = null,
superInterfacesFqn: RealmList<String>? = null,
apiInfo: ApiInfo? = null,
fields: RealmList<JavaField>? = null,
methods: RealmList<JavaMethod>? = null
): JavaClass {
@@ -95,7 +90,6 @@ open class JavaClass : IJavaType<JavaField, JavaMethod> {
this.superClassFqn = superClassFqn
this.superInterfacesFqn = superInterfacesFqn
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.fields = fields
this.methods = methods
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.itsaky.androidide.lsp.java.indexing.IJavaType
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import io.realm.RealmList
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
@@ -60,9 +59,6 @@ open class JavaEnum : IJavaType<JavaField, JavaMethod> {
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("fields")
override var fields: RealmList<JavaField>? = null
@@ -82,7 +78,6 @@ open class JavaEnum : IJavaType<JavaField, JavaMethod> {
isInner: Boolean = false,
superClassFqn: String? = null,
superInterfacesFqn: RealmList<String>? = null,
apiInfo: ApiInfo? = null,
fields: RealmList<JavaField>? = null,
methods: RealmList<JavaMethod>? = null
): JavaEnum {
@@ -94,7 +89,6 @@ open class JavaEnum : IJavaType<JavaField, JavaMethod> {
this.superClassFqn = superClassFqn
this.superInterfacesFqn = superInterfacesFqn
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.fields = fields
this.methods = methods
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.itsaky.androidide.lsp.java.indexing.IJavaSymbol
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
@@ -41,9 +40,6 @@ open class JavaField : IJavaSymbol {
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("constantValue")
var constantValue: JavaConstant? = null
@@ -56,14 +52,12 @@ open class JavaField : IJavaSymbol {
name: String,
type: JavaType,
accessFlags: Int,
apiInfo: ApiInfo? = null,
constantValue: JavaConstant? = null
): JavaField {
return JavaField().apply {
this.name = name
this.type = type
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.constantValue = constantValue
}
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.itsaky.androidide.lsp.java.indexing.IJavaType
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import io.realm.RealmList
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
@@ -61,9 +60,6 @@ open class JavaInterface : IJavaType<JavaField, JavaMethod> {
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("fields")
override var fields: RealmList<JavaField>? = null
@@ -82,7 +78,6 @@ open class JavaInterface : IJavaType<JavaField, JavaMethod> {
accessFlags: Int,
isInner: Boolean = false,
superInterfacesFqn: RealmList<String>? = null,
apiInfo: ApiInfo? = null,
fields: RealmList<JavaField>? = null,
methods: RealmList<JavaMethod>? = null
): JavaInterface {
@@ -93,7 +88,6 @@ open class JavaInterface : IJavaType<JavaField, JavaMethod> {
this.isInner = isInner
this.superInterfacesFqn = superInterfacesFqn
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.fields = fields
this.methods = methods
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.itsaky.androidide.lsp.java.indexing.IJavaSymbol
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import io.realm.RealmList
import io.realm.RealmResults
import io.realm.annotations.Index
@@ -47,9 +46,6 @@ open class JavaMethod : IJavaSymbol {
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@LinkingObjects("methods")
val ofClass: RealmResults<JavaClass>? = null
@@ -75,14 +71,12 @@ open class JavaMethod : IJavaSymbol {
paramsTypes: RealmList<JavaType>,
returnType: JavaType,
accessFlags: Int,
apiInfo: ApiInfo? = null,
): JavaMethod {
return JavaMethod().apply {
this.name = name
this.paramsTypes = paramsTypes
this.returnType = returnType
this.accessFlags = accessFlags
this.apiInfo = apiInfo
}
}
}
@@ -18,8 +18,8 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.google.common.base.Objects
import com.itsaky.androidide.lsp.java.indexing.ICloneable
import com.itsaky.androidide.lsp.java.indexing.ISharedJavaIndexable
import com.itsaky.androidide.models.ICloneable
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
@@ -146,7 +146,7 @@ open class JavaType : ISharedJavaIndexable, ICloneable {
override fun toString(): String {
return "JavaType(id=$id, name=$name, kind=$kind, arrayDims=$arrayDims)"
}
companion object {
const val KIND_UNKNOWN = -1
const val KIND_BOOLEAN = 0
@@ -163,7 +163,7 @@ open class JavaType : ISharedJavaIndexable, ICloneable {
// kind values used in IAnnotationElementValue
// defined here for convenience
internal const val __KIND_STRING = 10
internal const val __KIND_CLASS = 11
internal const val __KIND_CLASS = 11
internal const val __KIND_ENUM = 12
internal const val __KIND_ANNOTATION = 13
internal const val __KIND_ARRAY = 14
@@ -18,8 +18,8 @@
package com.itsaky.androidide.lsp.java.indexing.classfile
import com.google.common.base.Objects
import com.itsaky.androidide.db.utils.isEqualTo
import com.itsaky.androidide.db.utils.hash
import com.itsaky.androidide.db.utils.isEqualTo
import com.itsaky.androidide.lsp.java.indexing.ISharedJavaIndexable
import io.realm.RealmAny
import io.realm.RealmDictionary
@@ -15,13 +15,28 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.java.indexing
package com.itsaky.androidide.lsp.java.indexing.internal
import com.google.auto.service.AutoService
import com.itsaky.androidide.indexing.IIndexService
import com.itsaky.androidide.progress.IProgressIndicator
import com.itsaky.androidide.projects.api.Project
import java.io.File
/**
* Anything that can be cloned.
*
* @author Akash Yadav
*/
interface ICloneable {
fun clone(): ICloneable
@AutoService(IIndexService::class)
internal class JavaIndexServiceImpl : IIndexService {
override fun scanFiles(project: Project): Collection<File> {
return emptySet()
}
override suspend fun indexFiles(
project: Project,
progress: IProgressIndicator,
files: Collection<File>
) {
}
}
@@ -1,342 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.java.indexing.apiinfo
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.testing.common.utils.findAndroidHome
import jaxp.xml.stream.XMLStreamException
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.ByteArrayInputStream
import java.io.File
/**
* @author Akash Yadav
*/
@RunWith(RobolectricTestRunner::class)
class ApiInfoParserTest {
val apiVersionXml by lazy {
val platformsDir = File(findAndroidHome(), "platforms")
val platformDir = platformsDir.listFiles()!!.asSequence().sortedByDescending {
// android-30, android-31, android-32, android-33, etc
it.name.substring("android-".length).toIntOrNull() ?: 0
}.first()
platformDir.resolve("data/api-versions.xml")
}
private lateinit var parser: ApiInfoParser
@Before
fun setup() {
parser = ApiInfoParser()
}
@Test
fun `test basic parse`() {
parser.parse(apiVersionXml.inputStream().buffered())
val manifestInfo = parser.getClassInfo("android/Manifest")
assertThat(manifestInfo).isNotNull()
assertThat(manifestInfo?.since).isEqualTo(1)
assertThat(manifestInfo?.deprecatedIn).isEqualTo(0)
assertThat(manifestInfo?.removedIn).isEqualTo(0)
}
@Test
fun testSingleClassParsing() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
parser.parse(inputStream)
val classInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo(1)
assertThat(classInfo.deprecatedIn).isEqualTo(0)
assertThat(classInfo.removedIn).isEqualTo(0)
}
@Test
fun testSingleClassWithMembersParsing() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0">
<field name="fieldA" since="2" deprecated="0" removed="0"/>
<method name="methodA" since="3" deprecated="0" removed="0"/>
</class>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
parser.parse(inputStream)
val classInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo(1)
val fieldInfo = parser.getMemberInfo("com.example.ClassA", "fieldA")
assertThat(fieldInfo).isNotNull()
assertThat(fieldInfo!!.since).isEqualTo(2)
val methodInfo = parser.getMemberInfo("com.example.ClassA", "methodA")
assertThat(methodInfo).isNotNull()
assertThat(methodInfo!!.since).isEqualTo(3)
}
@Test
fun testMultipleClassesParsing() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
<class name="com.example.ClassB" since="2" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
parser.parse(inputStream)
val classAInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classAInfo).isNotNull()
assertThat(classAInfo!!.since).isEqualTo(1)
val classBInfo = parser.getClassInfo("com.example.ClassB")
assertThat(classBInfo).isNotNull()
assertThat(classBInfo!!.since).isEqualTo(2)
}
@Test
fun testClassWithDeprecatedAndRemovedAttributes() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="30" removed="34"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
parser.parse(inputStream)
val classInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo(1)
assertThat(classInfo.deprecatedIn).isEqualTo(30)
assertThat(classInfo.removedIn).isEqualTo(34)
}
@Test
fun testClassWithInvalidAttributes() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="256" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
try {
parser.parse(inputStream)
assertThat(true).isFalse() // Should not reach here
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().contains("Invalid version")
}
}
@Test
fun testMissingNameAttribute() {
val xml = """
<api version="34">
<class since="1" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
try {
parser.parse(inputStream)
assertThat(true).isFalse() // Should not reach here
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().contains("Missing name attribute")
}
}
@Test
fun testDuplicateClassEntry() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
<class name="com.example.ClassA" since="2" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
try {
parser.parse(inputStream)
assertThat(true).isFalse() // Should not reach here
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().contains("Duplicate class entry")
}
}
@Test
fun testDuplicateMemberEntry() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0">
<field name="fieldA" since="2" deprecated="0" removed="0"/>
<field name="fieldA" since="3" deprecated="0" removed="0"/>
</class>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
try {
parser.parse(inputStream)
assertThat(true).isFalse() // Should not reach here
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().contains("Duplicate field entry in class com.example.ClassA")
}
}
@Test
fun testMultipleApiElements() {
val xml = """
<api version="33">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
</api>
<api version="34">
<class name="com.example.ClassB" since="2" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = ApiInfoParser()
assertThrows("The markup in the document following the root element must be well-formed.", XMLStreamException::class.java) {
parser.parse(inputStream)
}
}
@Test
fun testNestedClassElements() {
val xml = """
<api version="34">
<class name="com.example.OuterClass" since="1" deprecated="0" removed="0">
<class name="com.example.OuterClass.InnerClass" since="2" deprecated="0" removed="0"/>
</class>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
assertThrows("<class> elements cannot be nested", IllegalStateException::class.java) {
parser.parse(inputStream)
}
}
@Test
fun testBoundaryAttributeValues() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="255" removed="0"/>
<class name="com.example.ClassB" since="1" deprecated="0" removed="255"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
parser.parse(inputStream)
val classAInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classAInfo).isNotNull()
assertThat(classAInfo!!.deprecatedIn).isEqualTo(255)
val classBInfo = parser.getClassInfo("com.example.ClassB")
assertThat(classBInfo).isNotNull()
assertThat(classBInfo!!.removedIn).isEqualTo(255)
}
@Test
fun testOutOfBoundaryAttributeValues() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="256" removed="0"/>
<class name="com.example.ClassB" since="1" deprecated="0" removed="256"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
assertThrows("Invalid version: 1, 256, 0", IllegalStateException::class.java) {
parser.parse(inputStream)
}
}
@Test
fun testUnexpectedElements() {
val xml = """
<api version="34">
<unexpectedElement name="unexpected" since="1" deprecated="0" removed="0"/>
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
parser.parse(inputStream)
val classInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo(1)
}
@Test
fun testEmptyXmlFile() {
val xml = ""
val inputStream = ByteArrayInputStream(xml.toByteArray())
parser.parse(inputStream)
}
@Test
fun testLargeInputFile() {
val xmlBuilder = StringBuilder()
xmlBuilder.append("<api version=\"34\">")
for (i in 1..1000) {
xmlBuilder.append("<class name=\"com.example.Class$i\" since=\"${(i % 254) + 1}\" />")
}
xmlBuilder.append("</api>")
val inputStream = ByteArrayInputStream(xmlBuilder.toString().toByteArray())
val parser = ApiInfoParser()
parser.parse(inputStream)
for (i in 1..1000) {
val classInfo = parser.getClassInfo("com.example.Class$i")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo((i % 254) + 1)
assertThat(classInfo.deprecatedIn).isEqualTo(0)
assertThat(classInfo.removedIn).isEqualTo(0)
}
}
}