fix: add base files for the indexing API

This commit is contained in:
Akash Yadav
2024-05-25 01:27:07 +05:30
parent 87ced38d26
commit 12ab4b0d15
45 changed files with 3627 additions and 70 deletions
+8 -2
View File
@@ -12,6 +12,7 @@ plugins {
id("kotlin-android")
id("kotlin-kapt")
id("kotlin-parcelize")
id("realm-android")
id("androidx.navigation.safeargs.kotlin")
id("com.itsaky.androidide.desugaring")
}
@@ -51,7 +52,11 @@ android {
}
}
kapt { arguments { arg("eventBusIndex", "${BuildConfig.packageName}.events.AppEventsIndex") } }
kapt {
arguments {
arg("eventBusIndex", "${BuildConfig.packageName}.events.AppEventsIndex")
}
}
desugaring {
replacements {
@@ -142,6 +147,7 @@ dependencies {
implementation(projects.gradlePluginConfig)
implementation(projects.idestats)
implementation(projects.subprojects.aaptcompiler)
implementation(projects.subprojects.indexing)
implementation(projects.subprojects.javacServices)
implementation(projects.subprojects.xmlUtils)
implementation(projects.subprojects.projects)
@@ -166,4 +172,4 @@ dependencies {
testImplementation(projects.testing.unit)
androidTestImplementation(projects.testing.android)
}
}
@@ -49,7 +49,6 @@ import androidx.core.view.updatePaddingRelative
import com.blankj.utilcode.constant.MemoryConstants
import com.blankj.utilcode.util.ConvertUtils.byte2MemorySize
import com.blankj.utilcode.util.FileUtils
import com.blankj.utilcode.util.KeyboardUtils
import com.blankj.utilcode.util.ThreadUtils
import com.github.mikephil.charting.components.AxisBase
import com.github.mikephil.charting.data.Entry
@@ -30,7 +30,6 @@ import androidx.work.NetworkType
import androidx.work.Operation
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import ch.qos.logback.classic.Logger
import com.blankj.utilcode.util.ThrowableUtils.getFullStackTrace
import com.google.android.material.color.DynamicColors
import com.itsaky.androidide.BuildConfig
@@ -44,8 +43,6 @@ import com.itsaky.androidide.events.EditorEventsIndex
import com.itsaky.androidide.events.LspApiEventsIndex
import com.itsaky.androidide.events.LspJavaEventsIndex
import com.itsaky.androidide.events.ProjectsApiEventsIndex
import com.itsaky.androidide.logging.LogcatAppender
import com.itsaky.androidide.logging.encoder.IDELogFormatEncoder
import com.itsaky.androidide.preferences.internal.DevOpsPreferences
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.preferences.internal.StatPreferences
@@ -62,18 +59,19 @@ import com.itsaky.androidide.utils.flashError
import com.termux.app.TermuxApplication
import com.termux.shared.reflection.ReflectionUtils
import io.github.rosemoe.sora.widget.schemes.EditorColorScheme
import io.realm.Realm
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.lsposed.hiddenapibypass.HiddenApiBypass
import org.slf4j.LoggerFactory
import java.lang.Thread.UncaughtExceptionHandler
import java.time.Duration
import kotlin.system.exitProcess
class IDEApplication : TermuxApplication() {
private var uncaughtExceptionHandler: UncaughtExceptionHandler? = null
@@ -97,7 +95,8 @@ class IDEApplication : TermuxApplication() {
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()).penaltyLog().detectAll().build())
StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()).penaltyLog().detectAll().build()
)
if (DevOpsPreferences.dumpLogs) {
startLogcatReader()
}
@@ -109,6 +108,8 @@ class IDEApplication : TermuxApplication() {
EventBus.getDefault().register(this)
Realm.init(this)
AppCompatDelegate.setDefaultNightMode(GeneralPreferences.uiMode)
if (IThemeManager.getInstance().getCurrentTheme() == IDETheme.MATERIAL_YOU) {
@@ -123,26 +124,6 @@ class IDEApplication : TermuxApplication() {
}
}
private fun handleCrash(thread: Thread, th: Throwable) {
writeException(th)
try {
val intent = Intent()
intent.action = CrashHandlerActivity.REPORT_ACTION
intent.putExtra(CrashHandlerActivity.TRACE_KEY, getFullStackTrace(th))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
if (uncaughtExceptionHandler != null) {
uncaughtExceptionHandler!!.uncaughtException(thread, th)
}
exitProcess(1)
} catch (error: Throwable) {
log.error("Unable to show crash handler activity", error)
}
}
fun showChangelog() {
val intent = Intent(Intent.ACTION_VIEW)
var version = BuildInfo.VERSION_NAME_SIMPLE
@@ -168,14 +149,17 @@ class IDEApplication : TermuxApplication() {
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val request = PeriodicWorkRequestBuilder<StatUploadWorker>(Duration.ofHours(24)).setInputData(
AndroidIDEStats.statData.toInputData()).setConstraints(constraints)
AndroidIDEStats.statData.toInputData()
).setConstraints(constraints)
.addTag(StatUploadWorker.WORKER_WORK_NAME).build()
val workManager = WorkManager.getInstance(this)
log.info("reportStatsIfNecessary: Enqueuing StatUploadWorker...")
val operation = workManager.enqueueUniquePeriodicWork(StatUploadWorker.WORKER_WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE, request)
val operation = workManager.enqueueUniquePeriodicWork(
StatUploadWorker.WORKER_WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE, request
)
operation.state.observeForever(object : Observer<Operation.State> {
override fun onChanged(value: Operation.State) {
@@ -185,22 +169,6 @@ class IDEApplication : TermuxApplication() {
})
}
private fun startLogcatReader() {
if (ideLogcatReader != null) {
// already started
return
}
log.info("Starting logcat reader...")
ideLogcatReader = IDELogcatReader().also { it.start() }
}
private fun stopLogcatReader() {
log.info("Stopping logcat reader...")
ideLogcatReader?.stop()
ideLogcatReader = null
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPrefChanged(event: PreferenceChangeEvent) {
val enabled = event.value as? Boolean?
@@ -230,6 +198,26 @@ class IDEApplication : TermuxApplication() {
}
}
private fun handleCrash(thread: Thread, th: Throwable) {
writeException(th)
try {
val intent = Intent()
intent.action = CrashHandlerActivity.REPORT_ACTION
intent.putExtra(CrashHandlerActivity.TRACE_KEY, getFullStackTrace(th))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
if (uncaughtExceptionHandler != null) {
uncaughtExceptionHandler!!.uncaughtException(thread, th)
}
exitProcess(1)
} catch (error: Throwable) {
log.error("Unable to show crash handler activity", error)
}
}
private fun cancelStatUploadWorker() {
log.info("Opted-out of stat collection. Cancelling StatUploadWorker if enqueued...")
val operation = WorkManager.getInstance(this)
@@ -242,6 +230,22 @@ class IDEApplication : TermuxApplication() {
})
}
private fun startLogcatReader() {
if (ideLogcatReader != null) {
// already started
return
}
log.info("Starting logcat reader...")
ideLogcatReader = IDELogcatReader().also { it.start() }
}
private fun stopLogcatReader() {
log.info("Stopping logcat reader...")
ideLogcatReader?.stop()
ideLogcatReader = null
}
companion object {
private val log = LoggerFactory.getLogger(IDEApplication::class.java)
@@ -0,0 +1,43 @@
/*
* 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.indexing
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* An entity to store information about all registered box stores.
*
* @author Akash Yadav
*/
data class DatabaseEntity(
@Required
@PrimaryKey
@RealmField("path")
var path: String = "",
@Required
@RealmField("name")
var name: String = "",
@Required
@RealmField("directory")
var directory: String = "",
) : RealmObject()
@@ -0,0 +1,104 @@
/*
* 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.indexing
import com.google.auto.service.AutoService
import com.itsaky.androidide.utils.Environment
import io.realm.Realm
import io.realm.RealmConfiguration
import org.slf4j.LoggerFactory
/**
* Default implementation of [IRealmProvider].
*
* @author Akash Yadav
*/
@AutoService(IRealmProvider::class)
@Suppress("unused")
class RealmProviderImpl : IRealmProvider {
companion object {
private val log = LoggerFactory.getLogger(RealmProviderImpl::class.java)
// must be a relative path, with no special characters except '_' '-' and '/' (path sep)
private val dbNameRegex = Regex("^[a-zA-Z0-9_\\-/]+$")
private const val MASTER_DB_NAME = "master"
private const val MASTER_DB_PATH = "/${MASTER_DB_NAME}"
}
private val masterDb = createDb(MASTER_DB_PATH, null)
override fun get(path: String, config: (RealmConfiguration.Builder.() -> Unit)?): Realm {
require(path != MASTER_DB_PATH) {
"Master DB is internal to AndroidIDE"
}
return createDb(path, config)
}
private fun createDb(path: String, confFunc: (RealmConfiguration.Builder.() -> Unit)?): Realm {
val (parentPath, name) = validateDbPath(path)
val configBuilder = RealmConfiguration.Builder()
confFunc?.also { configure -> configBuilder.configure() }
var dbDir = Environment.REALM_DB_DIR
if (parentPath.isNotEmpty()) {
dbDir = dbDir.resolve(parentPath)
}
val dbName = "${name}.realm"
val config = configBuilder
.name(dbName)
.directory(dbDir)
.allowQueriesOnUiThread(false)
.allowWritesOnUiThread(false)
.compactOnLaunch()
.build()
log.info("Creating DB at $dbDir/$dbName")
masterDb.executeTransactionAsync { realm ->
realm.insertOrUpdate(DatabaseEntity(name = name, path = path, directory = dbDir.absolutePath))
}
return Realm.getInstance(config)
}
private fun validateDbPath(path: String): Pair<String, String> {
require(path.matches(dbNameRegex)) {
"Invalid DB path: $path"
}
val idx = path.indexOfLast { it == IRealmProvider.PATH_SEPARATOR }
require(idx > 0) {
"Invalid DB path: $path"
}
val parentPath = path.substring(0, idx)
val name = path.substring(idx + 1)
if (name == MASTER_DB_NAME) {
require(parentPath.isEmpty()) {
"Master DB must not have any parent path"
}
}
return parentPath to name
}
}
+1
View File
@@ -41,6 +41,7 @@ buildscript {
dependencies {
classpath(libs.kotlin.gradle.plugin)
classpath(libs.nav.safe.args.gradle.plugin)
classpath(libs.realm.gradle.plugin)
}
}
@@ -17,13 +17,17 @@
package com.itsaky.androidide.utils;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressLint("SdCardPath")
public final class Environment {
@@ -47,6 +51,7 @@ public final class Environment {
public static File BIN_DIR;
public static File LIB_DIR;
public static File PROJECTS_DIR;
public static File REALM_DB_DIR;
/**
* Used by Java LSP until the project is initialized.
@@ -64,8 +69,8 @@ public final class Environment {
public static void init() {
ROOT = mkdirIfNotExits(new File(DEFAULT_ROOT));
PREFIX = mkdirIfNotExits(new File(ROOT, "usr"));
HOME = mkdirIfNotExits(new File(ROOT, "home"));
PREFIX = mkdirIfNotExits(new File(DEFAULT_PREFIX));
HOME = mkdirIfNotExits(new File(DEFAULT_HOME));
ANDROIDIDE_HOME = mkdirIfNotExits(new File(HOME, ".androidide"));
TMP_DIR = mkdirIfNotExits(new File(PREFIX, "tmp"));
BIN_DIR = mkdirIfNotExits(new File(PREFIX, "bin"));
@@ -73,9 +78,10 @@ public final class Environment {
PROJECTS_DIR = mkdirIfNotExits(new File(FileUtil.getExternalStorageDir(), PROJECTS_FOLDER));
ANDROID_JAR = mkdirIfNotExits(new File(ANDROIDIDE_HOME, "android.jar"));
TOOLING_API_JAR = new File(mkdirIfNotExits(new File(ANDROIDIDE_HOME, "tooling-api")),
"tooling-api-all.jar");
"tooling-api-all.jar");
AAPT2 = new File(ANDROIDIDE_HOME, "aapt2");
ANDROIDIDE_UI = mkdirIfNotExits(new File(ANDROIDIDE_HOME, "ui"));
REALM_DB_DIR = mkdirIfNotExits(new File(ROOT, "realm-dbs"));
INIT_SCRIPT = new File(mkdirIfNotExits(new File(ANDROIDIDE_HOME, "init")), "init.gradle");
GRADLE_USER_HOME = new File(HOME, ".gradle");
+3
View File
@@ -18,6 +18,7 @@ retrofit = "2.9.0"
markwon = "4.6.2"
maven-publish-plugin = "0.27.0"
logback = "1.5.3"
realm = "10.18.0"
[libraries]
@@ -156,6 +157,8 @@ tooling-builderModel = { module = "com.android.tools.build:builder-model", versi
tooling-gradleApi = { module = "com.itsaky.androidide.gradle:gradle-tooling-api", version.ref = "gradle-tooling" }
tooling-slf4j = { module = "org.slf4j:slf4j-api", version = "2.0.12" }
realm-gradle-plugin = { module = "io.realm:realm-gradle-plugin", version.ref = "realm" }
# Classpaths
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+10
View File
@@ -21,10 +21,17 @@ plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-kapt")
id("realm-android")
}
android {
namespace = "${BuildConfig.packageName}.lsp.java"
sourceSets {
getByName("androidTest") {
assets.srcDirs(rootProject.file("subprojects/framework-stubs/libs"))
}
}
}
kapt {
@@ -54,9 +61,11 @@ dependencies {
implementation(projects.editorApi)
implementation(projects.resources)
implementation(projects.lsp.api)
implementation(projects.subprojects.indexing)
implementation(projects.subprojects.javacServices)
implementation(libs.composite.javac)
implementation(libs.composite.jdkJdeps)
implementation(libs.composite.javapoet)
implementation(libs.composite.googleJavaFormat)
@@ -64,4 +73,5 @@ dependencies {
implementation(libs.common.kotlin)
testImplementation(projects.testing.lsp)
androidTestImplementation(projects.testing.android)
}
@@ -0,0 +1,102 @@
/*
* 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 androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmModel
import io.realm.log.LogLevel
import io.realm.log.RealmLog
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
/**
* @author Akash Yadav
*/
@RunWith(AndroidJUnit4::class)
class JavaIndexerTest {
@Test
fun testSimpleRealmInit() {
val context = InstrumentationRegistry.getInstrumentation().context
Realm.init(context.applicationContext)
val file = File.createTempFile("ajar", null, context.cacheDir)
context.assets.open("android.jar").use { asset ->
assertThat(asset).isNotNull()
file.outputStream().buffered().use { out ->
asset.copyTo(out)
out.flush()
}
}
val dbDir = context.cacheDir.resolve("realm-db")
if (dbDir.exists()) {
if (dbDir.isDirectory) {
dbDir.deleteRecursively()
} else {
dbDir.delete()
}
}
RealmLog.setLevel(LogLevel.ALL)
val realm = Realm.getInstance(
RealmConfiguration.Builder()
.directory(dbDir)
.name("android-jar-classes.realm")
.deleteRealmIfMigrationNeeded()
.build()
)
try {
val start = System.currentTimeMillis()
val worker = JavaIndexWorker(file)
val batches = mutableMapOf<Class<*>, MutableList<RealmModel>>()
worker.consumeTypes { type ->
val batched = batches.computeIfAbsent(type.javaClass) { mutableListOf() }
batched.add(type)
if (batched.size >= 100) {
realm.executeTransaction {
realm.insertOrUpdate(batched)
}
batched.clear()
}
}
for ((_, batched) in batches) {
if (batched.isNotEmpty()) {
realm.executeTransaction {
realm.insertOrUpdate(batched)
}
}
}
println("Took ${System.currentTimeMillis() - start}ms")
} finally {
realm.close()
file.delete()
}
}
}
@@ -0,0 +1,232 @@
/*
* 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.lsp.java.indexing.models.AnnotationElement
import com.itsaky.androidide.lsp.java.indexing.models.IJavaType
import com.itsaky.androidide.lsp.java.indexing.models.JavaField
import com.itsaky.androidide.lsp.java.indexing.models.JavaMethod
import com.itsaky.androidide.lsp.java.indexing.models.JavaType
import java.util.Objects
/**
* @author Akash Yadav
*/
object DescriptorUtils {
/**
* Create a method ID for the given method.
*/
fun createMethodId(
klass: IJavaType<*, *>,
method: JavaMethod
): Int {
return Objects.hash(klass.fqn, method.name, method.paramsTypes, method.returnType)
}
/**
* Create a method ID for the given method.
*/
fun createMethodId(
klass: IJavaType<*, *>,
element: AnnotationElement
): Int {
return Objects.hash(klass.fqn, element.name, element.type)
}
/**
* Create a field ID for the given field. The string representation is similar to how fields
* are represented in Smali.
*
* ```
* Lcom/example/Foo;->bar:I
* ```
*/
fun createFieldId(
klass: IJavaType<*, *>,
field: JavaField
): Int {
return Objects.hash(klass.fqn, field.name, field.type)
}
/**
* Parse the return type of a method descriptor.
*
* @param descriptor The method descriptor.
*/
fun returnType(descriptor: String): JavaType {
val idx = descriptor.indexOfLast { it == ')' }
if (descriptor[0] != '(' || idx == -1) {
throw IllegalArgumentException("Invalid method descriptor: $descriptor")
}
return type(descriptor.substring(idx + 1))
}
/**
* Parse the parameter types of a method descriptor.
*
* @param descriptor The method descriptor.
*/
fun paramTypes(descriptor: String): List<JavaType> {
val idx = descriptor.indexOfLast { it == ')' }
if (descriptor[0] != '(' || idx == -1) {
throw IllegalArgumentException("Invalid method descriptor: $descriptor")
}
val types = mutableListOf<JavaType>()
val params = descriptor.substring(1, idx)
var i = 0
while (i < params.length) {
types.add(
when (params[i]) {
'L' -> {
val end = params.indexOf(';', startIndex = i + 1)
referenceType(params, fromIndex = i, toIndex = end).also {
i = end + 1
}
}
'[' -> arrayType(params, fromIndex = i).also {
// number of '['
// + length of type (I or java/lang/String)
// + 2 if kind is ref (because refs have 'L' and ';')
i += it.arrayDims + it.name!!.length + (if (it.kind == JavaType.KIND_REF) 2 else 0)
}
else -> primitive(params[i].toString()).also {
++i
}
}
)
}
return types
}
/**
* Parse a type descriptor. A type descriptor can be any of :
* - a primitive type descriptor
* - a reference type descriptor
* - an array type descriptor
*
* @param descriptor The type descriptor.
*/
fun type(descriptor: String): JavaType {
if (descriptor.startsWith("L")) {
return referenceType(descriptor)
}
if (descriptor.startsWith("[")) {
return arrayType(descriptor)
}
if (descriptor.length != 1) {
throw IllegalArgumentException("Invalid type descriptor: $descriptor")
}
return primitive(descriptor)
}
/**
* Parse a primitive type descriptor.
*
* @param descriptor The primitive type descriptor.
*/
fun primitive(descriptor: String): JavaType = primitive(descriptor[0])
/**
* Parse a primitive type descriptor.
*
* @param char The primitive type.
*/
fun primitive(char: Char): JavaType {
val primitive = JavaType.primitiveFor(char)
return requireNotNull(primitive) {
"Invalid type descriptor: $char"
}
}
/**
* Parse a reference type descriptor.
*
* Format:
* ```
* Lcom/example/Foo;
* ```
*
* @param descriptor The descriptor of a reference type
* @param fromIndex The index of the first character of the reference type name.
* @param toIndex The index of the last character of the reference type name (inclusive).
*/
fun referenceType(
descriptor: String, fromIndex: Int = 0, toIndex: Int = descriptor.length - 1
): JavaType {
if (descriptor[fromIndex] != 'L' || descriptor[toIndex] != ';') {
throw IllegalArgumentException("Invalid reference type descriptor: $descriptor, fromIndex: $fromIndex, toIndex: $toIndex")
}
return when (descriptor) {
"Ljava/lang/String;" -> JavaType.STRING
"Ljava/lang/Object;" -> JavaType.OBJECT
"Ljava/lang/Class;" -> JavaType.CLASS
else -> {
// remove leading 'L' and trailing ';'
val name = descriptor.substring(fromIndex + 1, toIndex)
JavaType.newInstance(name = name, kind = JavaType.KIND_REF)
}
}
}
/**
* Parse an array type descriptor.
*
* Format:
* ```
* [I
* [[I
* [[Lcom/example/Foo;
* ```
*
* @param descriptor The array type descriptor.
* @param fromIndex The index of the first character of the array type descriptor.
*/
fun arrayType(descriptor: String, fromIndex: Int = 0): JavaType {
if (descriptor[fromIndex] != '[') {
throw IllegalArgumentException("Invalid array type descriptor: $descriptor, fromIndex: $fromIndex")
}
var idx = fromIndex + 1
var dims = 1
while (descriptor[idx] == '[') {
idx++
dims++
}
val type = if (descriptor[idx] == 'L') {
referenceType(
descriptor, fromIndex = idx, toIndex = descriptor.indexOf(';', startIndex = idx + 1)
)
} else {
primitive(descriptor[idx])
}
return JavaType.newInstance(name = type.name, kind = type.kind, arrayDims = dims)
}
}
@@ -0,0 +1,394 @@
/*
* 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.lsp.java.indexing.models.AnnotationAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.models.AnnotationElement
import com.itsaky.androidide.lsp.java.indexing.models.AnnotationElement.Companion.newAnnotationElement
import com.itsaky.androidide.lsp.java.indexing.models.ArrayAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.models.ClassAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.models.EnumAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.models.IAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.models.IJavaType
import com.itsaky.androidide.lsp.java.indexing.models.JavaAnnotation
import com.itsaky.androidide.lsp.java.indexing.models.JavaClass
import com.itsaky.androidide.lsp.java.indexing.models.JavaConstant
import com.itsaky.androidide.lsp.java.indexing.models.JavaEnum
import com.itsaky.androidide.lsp.java.indexing.models.JavaField
import com.itsaky.androidide.lsp.java.indexing.models.JavaInterface
import com.itsaky.androidide.lsp.java.indexing.models.JavaMethod
import com.itsaky.androidide.lsp.java.indexing.models.JavaType
import com.itsaky.androidide.lsp.java.indexing.models.PrimitiveAnnotationElementValue
import io.realm.RealmAny
import io.realm.RealmDictionary
import io.realm.RealmList
import openjdk.tools.classfile.AccessFlags
import openjdk.tools.classfile.Annotation.Annotation_element_value
import openjdk.tools.classfile.Annotation.Array_element_value
import openjdk.tools.classfile.Annotation.Class_element_value
import openjdk.tools.classfile.Annotation.Enum_element_value
import openjdk.tools.classfile.Annotation.Primitive_element_value
import openjdk.tools.classfile.Annotation.element_value
import openjdk.tools.classfile.AnnotationDefault_attribute
import openjdk.tools.classfile.Attribute
import openjdk.tools.classfile.ClassFile
import openjdk.tools.classfile.ConstantPool
import openjdk.tools.classfile.ConstantPool.CONSTANT_Double_info
import openjdk.tools.classfile.ConstantPool.CONSTANT_Float_info
import openjdk.tools.classfile.ConstantPool.CONSTANT_Integer_info
import openjdk.tools.classfile.ConstantPool.CONSTANT_Long_info
import openjdk.tools.classfile.ConstantPool.CONSTANT_String_info
import openjdk.tools.classfile.ConstantPool.CONSTANT_Utf8_info
import openjdk.tools.classfile.ConstantPool.CPInfo
import openjdk.tools.classfile.ConstantValue_attribute
import java.io.File
import java.util.zip.ZipFile
/**
* @author Akash Yadav
*/
class JavaIndexWorker(
private val jar: File
) {
fun buildTypes(): List<IJavaType<*, *>> {
val types = mutableListOf<IJavaType<*, *>>()
consumeTypes { types.add(it) }
return types
}
fun consumeTypes(consumer: (IJavaType<*, *>) -> Unit) {
ZipFile(jar).use { jar ->
for (entry in jar.entries()) {
if (!entry.name.endsWith(".class")) {
continue
}
jar.getInputStream(entry).use { inputStream ->
val classFile = ClassFile.read(inputStream)!!
val type = buildType(classFile)
consumer(type)
}
}
}
}
private fun buildType(classFile: ClassFile): IJavaType<*, *> {
val classInfo = classFile.constant_pool.getClassInfo(classFile.this_class)
val fqn = classInfo.baseName
val name = simpleName(fqn)
val pck = fqn.substring(0, fqn.length - name.length - 1)
return buildType(fqn, name, pck, classFile)
}
private fun buildType(
fqn: String, name: String, pck: String, classFile: ClassFile
): IJavaType<*, *> {
if (classFile.access_flags.`is`(AccessFlags.ACC_ENUM)) {
return buildEnum(fqn, name, pck, classFile)
}
if (classFile.access_flags.`is`(AccessFlags.ACC_ANNOTATION)) {
return buildAnnotation(fqn, name, pck, classFile)
}
if (classFile.access_flags.`is`(AccessFlags.ACC_INTERFACE)) {
return buildInterface(fqn, name, pck, classFile)
}
return buildClass(fqn, name, pck, classFile)
}
private fun buildClass(fqn: String, name: String, pck: String, classFile: ClassFile): JavaClass {
return JavaClass.newInstance(fqn, name, pck, classFile.access_flags.flags).apply {
setSuperClass(classFile)
setSuperInterfaces(classFile)
setFields(classFile)
setMethods(classFile)
}
}
private fun buildInterface(
fqn: String, name: String, pck: String, classFile: ClassFile
): JavaInterface {
return JavaInterface.newInstance(fqn, name, pck, classFile.access_flags.flags).apply {
setSuperClass(classFile)
setSuperInterfaces(classFile)
setFields(classFile)
setMethods(classFile)
}
}
private fun buildAnnotation(
fqn: String, name: String, pck: String, classFile: ClassFile
): JavaAnnotation {
return JavaAnnotation.newInstance(fqn, name, pck, classFile.access_flags.flags).apply {
setSuperClass(classFile)
setSuperInterfaces(classFile)
setFields(classFile)
setAnnotationElements(classFile)
}
}
private fun buildEnum(fqn: String, name: String, pck: String, classFile: ClassFile): JavaEnum {
return JavaEnum.newInstance(fqn, name, pck, classFile.access_flags.flags).apply {
setSuperClass(classFile)
setSuperInterfaces(classFile)
setFields(classFile)
setMethods(classFile)
}
}
private fun IJavaType<*, *>.setSuperInterfaces(classFile: ClassFile) {
val interfaces = RealmList<String>()
for (iface in classFile.interfaces) {
val ifaceInfo = classFile.constant_pool.getClassInfo(iface)
interfaces.add(ifaceInfo.baseName!!)
}
superInterfacesFqn = interfaces
}
private fun IJavaType<*, *>.setSuperClass(classFile: ClassFile) {
if (classFile.super_class == 0) {
// From https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.5
// If the value of the super_class field is 0, then this class represents the java/lang/Object class itself
// and hence, doesn't have a super class.
return
}
val superclassInfo = classFile.constant_pool.getClassInfo(classFile.super_class)
superClassFqn = superclassInfo.baseName!!
}
private fun IJavaType<*, JavaMethod>.setMethods(classFile: ClassFile) {
methods = RealmList<JavaMethod>().apply {
addAll(classFile.methods.map { method ->
val descriptor = classFile.constant_pool.getUTF8Value(method.descriptor.index)
val methodName = method.getName(classFile.constant_pool)
val methodType = DescriptorUtils.returnType(descriptor)
val paramTypes = RealmList<JavaType>().apply {
addAll(DescriptorUtils.paramTypes(descriptor))
}
JavaMethod.newInstance(methodName, paramTypes, methodType, method.access_flags.flags)
})
}
}
private fun IJavaType<JavaField, *>.setFields(classFile: ClassFile) {
fields = RealmList<JavaField>().apply {
addAll(classFile.fields.map { field ->
val descriptor = classFile.constant_pool.getUTF8Value(field.descriptor.index)
val fieldName = field.getName(classFile.constant_pool)
val fieldType = DescriptorUtils.type(descriptor)
JavaField.newField(fieldName, fieldType, field.access_flags.flags).apply {
field.attributes.get(Attribute.ConstantValue)?.also { constAttr ->
val constantValue = constAttr as ConstantValue_attribute
val constant = classFile.constant_pool.get(constantValue.constantvalue_index)
val isStringType = fieldType.isStringType()
val javaConstant = if (fieldType.isPrimitive() || isStringType) {
toJavaConstant(fieldType.kind, constant, if (isStringType) 's' else null)
} else {
throw IllegalArgumentException("Invalid field type for constant value: $fieldType")
}
this.constantValue = javaConstant
}
}
})
}
}
private fun JavaAnnotation.setAnnotationElements(classFile: ClassFile) {
methods = RealmList<AnnotationElement>().apply {
addAll(classFile.methods.map { method ->
val descriptor = classFile.constant_pool.getUTF8Value(method.descriptor.index)
val methodName = method.getName(classFile.constant_pool)
val methodType = DescriptorUtils.returnType(descriptor)
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()
}
}
})
}
}
private fun toAnnotationElementValue(
defaultValue: element_value, classFile: ClassFile
): IAnnotationElementValue {
val char = defaultValue.tag.toChar()
var kind = JavaType.kindForType(char)
if (char == 's') {
kind = JavaType.KIND_REF
}
var value: IAnnotationElementValue? = when (kind) {
JavaType.KIND_BOOLEAN,
JavaType.KIND_BYTE,
JavaType.KIND_CHAR,
JavaType.KIND_SHORT,
JavaType.KIND_INT,
JavaType.KIND_LONG,
JavaType.KIND_FLOAT,
JavaType.KIND_DOUBLE -> PrimitiveAnnotationElementValue.newInstance(
kind = when (kind) {
JavaType.KIND_BOOLEAN -> IAnnotationElementValue.KIND_BOOLEAN
JavaType.KIND_BYTE -> IAnnotationElementValue.KIND_BYTE
JavaType.KIND_CHAR -> IAnnotationElementValue.KIND_CHAR
JavaType.KIND_SHORT -> IAnnotationElementValue.KIND_SHORT
JavaType.KIND_INT -> IAnnotationElementValue.KIND_INT
JavaType.KIND_LONG -> IAnnotationElementValue.KIND_LONG
JavaType.KIND_FLOAT -> IAnnotationElementValue.KIND_FLOAT
JavaType.KIND_DOUBLE -> IAnnotationElementValue.KIND_DOUBLE
else -> throw IllegalArgumentException("Unknown value tag: $char")
},
value = toPrimitiveConstant(
classFile.constant_pool, kind, char, defaultValue as Primitive_element_value
)
)
else -> null
}
if (value == null) {
value = when (char) {
's' -> PrimitiveAnnotationElementValue.newInstance(
kind = IAnnotationElementValue.KIND_STRING, value = toPrimitiveConstant(
classFile.constant_pool,
JavaType.KIND_REF,
char,
defaultValue as Primitive_element_value
)
)
'e' -> {
val enum = defaultValue as Enum_element_value
EnumAnnotationElementValue.newInstance(
name = classFile.constant_pool.getUTF8Value(enum.const_name_index),
type = DescriptorUtils.type(classFile.constant_pool.getUTF8Value(enum.type_name_index))
)
}
'c' -> {
val clazz = defaultValue as Class_element_value
// even though the name is given as "class_info_index", the constant at that index
// is actually a CONSTANT_Utf8_info representing a return type descriptor (JLS $4.3.3)
// this has been clearly mentioned in the Java Language Specification
ClassAnnotationElementValue.newInstance(
DescriptorUtils.type(
classFile.constant_pool.getUTF8Value(
clazz.class_info_index
)
)
)
}
'@' -> {
val annotation = (defaultValue as Annotation_element_value).annotation_value
AnnotationAnnotationElementValue.newInstance(type = DescriptorUtils.type(
classFile.constant_pool.getUTF8Value(
annotation.type_index
)
),
values = RealmDictionary<RealmAny>().apply {
for (pair in annotation.element_value_pairs) {
this[classFile.constant_pool.getUTF8Value(pair.element_name_index)] =
toAnnotationElementValue(pair.value, classFile).let { elementValue ->
RealmAny.valueOf(elementValue)
}
}
})
}
'[' -> {
val arr = defaultValue as Array_element_value
ArrayAnnotationElementValue.newInstance(values = RealmList<RealmAny>().apply {
addAll(arr.values.map { element ->
toAnnotationElementValue(element, classFile).let { elementValue ->
RealmAny.valueOf(elementValue)
}
})
})
}
else -> throw IllegalArgumentException("Unknown value tag: ${defaultValue.tag}")
}
}
return value
}
private fun toPrimitiveConstant(
constantPool: ConstantPool, kind: Short, char: Char, prim: Primitive_element_value
): JavaConstant {
if (kind == JavaType.KIND_REF && char != 's') {
throw IllegalArgumentException("Unknown value tag: $char")
}
val constant = constantPool.get(prim.const_value_index)
return toJavaConstant(kind, constant, char)
}
private fun toJavaConstant(
kind: Short, constant: CPInfo, char: Char?
): JavaConstant {
val realmAny = when (kind) {
JavaType.KIND_BOOLEAN,
JavaType.KIND_BYTE,
JavaType.KIND_CHAR,
JavaType.KIND_SHORT,
JavaType.KIND_INT -> (constant as CONSTANT_Integer_info).value.let {
RealmAny.valueOf(it)
}
JavaType.KIND_LONG -> (constant as CONSTANT_Long_info).value.let { RealmAny.valueOf(it) }
JavaType.KIND_FLOAT -> (constant as CONSTANT_Float_info).value.let { RealmAny.valueOf(it) }
JavaType.KIND_DOUBLE -> (constant as CONSTANT_Double_info).value.let { RealmAny.valueOf(it) }
else -> if (char == 's') {
if (constant is CONSTANT_String_info) {
constant.string
} else {
(constant as CONSTANT_Utf8_info).value
}.let { RealmAny.valueOf(it) }
} else {
throw IllegalArgumentException("Unknown value tag: $char")
}
}
return JavaConstant.newInstance(
kind = kind,
value = realmAny
)
}
private fun simpleName(fqn: String): String {
var idx = fqn.lastIndexOf('$')
idx = if (idx == -1) fqn.lastIndexOf('/') else idx
return fqn.substring(idx + 1)
}
}
@@ -0,0 +1,57 @@
/*
* 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.lsp.java.indexing.models.IJavaIndexable
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 = JavaIndexWorker(jar)
// worker.index(index)
}
}
}
override fun close() {
coroutineScope.cancelIfActive("Cancellation requested")
}
}
@@ -0,0 +1,102 @@
/*
* 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.models
import com.itsaky.androidide.lsp.java.indexing.DescriptorUtils
import io.realm.RealmAny
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* A method defined in an annotation type.
*
* @property defaultValue The default value of the annotation element.
* @author Akash Yadav
*/
@RealmClass(embedded = true)
open class AnnotationElement : IJavaSymbol {
@Index
@Required
@RealmField("methodId")
var methodId: String? = null
@Index
@Required
@RealmField("name")
var name: String? = null
@RealmField("type")
var type: JavaType? = null
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@LinkingObjects("methods")
val ofClass: RealmResults<JavaAnnotation>? = null
/**
* The default value of the annotation element. One of the following:
* - [PrimitiveAnnotationElementValue]
* - [EnumAnnotationElementValue]
* - [ArrayAnnotationElementValue]
* - [ClassAnnotationElementValue]
* - [AnnotationAnnotationElementValue]
*/
@RealmField("defaultValue")
var defaultValue: RealmAny? = null
/**
* Returns the signature of the method.
*/
fun signature(): String {
val sb = StringBuilder()
sb.append(name)
sb.append("(")
sb.append(")")
sb.append(type?.internalForm())
return sb.toString()
}
companion object {
@JvmStatic
fun JavaAnnotation.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
}
}
}
}
@@ -0,0 +1,65 @@
/*
* 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.models
import com.google.common.base.Objects
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 {
@Required
@PrimaryKey
@RealmField("id")
override var id: Int? = null
@RealmField("since")
var since: Int = 1
@RealmField("deprecatedIn")
var deprecatedIn: Int = 0
@RealmField("removedIn")
var removedIn: Int = 0
override fun computeId() {
this.id = Objects.hashCode(this.since, this.removedIn, this.deprecatedIn)
}
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()
}
}
}
}
@@ -0,0 +1,25 @@
/*
* 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.models
import com.itsaky.androidide.indexing.IIndexable
/**
* @author Akash Yadav
*/
interface IJavaIndexable : IIndexable
@@ -0,0 +1,30 @@
/*
* 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.models
/**
* A java symbol.
*
* @property accessFlags Access flags of the symbol.
* @property apiInfo Info about the APIs in which this symbol was added, deprecated or removed in.
* @author Akash Yadav
*/
interface IJavaSymbol : IJavaIndexable {
var accessFlags: Int
var apiInfo: ApiInfo?
}
@@ -0,0 +1,72 @@
/*
* 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.models
import io.realm.RealmList
import io.realm.annotations.Required
/**
* A Java user type. May be a class, interface, enum, or annotation. Qualified name of a type or a
* package name is always represented in the (partially) internal form. For example, the fully qualified
* of `java.lang.Object` is `java/lang/Object` and the package name is `java/lang`.
*
* @property fqn The fully qualified name of the type.
* @property name The simple name of the type.
* @property packageName The package name of the type.
* @property superInterfacesFqn The fully qualified names of the interfaces implemented by the type.
* @property fields The fields of the type.
* @property methods The methods of the type.
* @property isClass Whether the type is a class.
* @property isInterface Whether the type is an interface.
* @property isAnnotation Whether the type is an annotation.
* @property isEnum Whether the type is an enum.
* @author Akash Yadav
*/
interface IJavaType<FieldT : IJavaSymbol, MethodT : IJavaSymbol> : IJavaSymbol {
var fqn: String?
var name: String?
var packageName: String?
/**
* The fully qualified name of the superclass of the type.
*
* Info about fields and methods are availabe in the class file itself. Hhowever, a class may
* extend a class (or implement an interface) which may be defined in other jar files. As a result,
* we only store the fully qualifed names of the classes and interfaces then add an index based on
* these fully qualifed names in the database.
*
* This property is not [Required] because we also index `java/lang/Object`, which does not have
* a superclass.
*/
var superClassFqn: String?
var superInterfacesFqn: RealmList<String>?
var fields: RealmList<FieldT>?
var methods: RealmList<MethodT>?
val isClass: Boolean
get() = false
val isInterface: Boolean
get() = false
val isAnnotation: Boolean
get() = false
val isEnum: Boolean
get() = false
}
@@ -0,0 +1,36 @@
/*
* 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.models
/**
* An [IJavaIndexable] model which is shared between other models.
*
* @author Akash Yadav
*/
interface ISharedJavaIndexable : IJavaIndexable {
/**
* A unique ID for the indexable.
*/
var id: Int?
/**
* Compute and update the ID of this indexable.
*/
fun computeId()
}
@@ -0,0 +1,104 @@
/*
* 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.models
import io.realm.RealmList
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* A Java annotation type.
*
* Implementation note:
* An annotation type is a Java interface, and hence, it inherits from [JavaInterface] instead of
* [IJavaType]. Also, [IJavaType.isInterface] and [IJavaType.isAnnotation] both
* return `true` for annotation types.
*
* @author Akash Yadav
*/
@RealmClass
open class JavaAnnotation : IJavaType<JavaField, AnnotationElement> {
@Index
@PrimaryKey
@Required
@RealmField("fqn")
override var fqn: String? = null
@Required
@RealmField("name")
override var name: String? = null
@Required
@RealmField("pck")
override var packageName: String? = null
@RealmField("superClassFqn")
override var superClassFqn: String? = null
@RealmField("superInterfacesFqn")
override var superInterfacesFqn: RealmList<String>? = null
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("fields")
override var fields: RealmList<JavaField>? = null
@RealmField("methods")
override var methods: RealmList<AnnotationElement>? = null
override val isInterface: Boolean
get() = true
override val isAnnotation: Boolean
get() = true
companion object {
@JvmStatic
fun newInstance(
fqn: String,
name: String,
packageName: String,
accessFlags: Int,
superInterfacesFqn: RealmList<String>? = null,
apiInfo: ApiInfo? = null,
fields: RealmList<JavaField>? = null,
methods: RealmList<AnnotationElement>? = null
): JavaAnnotation {
return JavaAnnotation().apply {
this.fqn = fqn
this.name = name
this.packageName = packageName
this.superClassFqn = superClassFqn
this.superInterfacesFqn = superInterfacesFqn
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.fields = fields
this.methods = methods
}
}
}
}
@@ -0,0 +1,97 @@
/*
* 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.models
import io.realm.RealmList
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* A Java class type.
*
* @author Akash Yadav
*/
@RealmClass
open class JavaClass : IJavaType<JavaField, JavaMethod> {
@Index
@PrimaryKey
@Required
@RealmField("fqn")
override var fqn: String? = null
@Required
@RealmField("name")
override var name: String? = null
@Required
@RealmField("pck")
override var packageName: String? = null
@RealmField("superClassFqn")
override var superClassFqn: String? = null
@RealmField("superInterfacesFqn")
override var superInterfacesFqn: RealmList<String>? = null
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("fields")
override var fields: RealmList<JavaField>? = null
@RealmField("methods")
override var methods: RealmList<JavaMethod>? = null
override val isClass: Boolean
get() = true
companion object {
@JvmStatic
fun newInstance(
fqn: String,
name: String,
packageName: String,
accessFlags: Int,
superClassFqn: String? = null,
superInterfacesFqn: RealmList<String>? = null,
apiInfo: ApiInfo? = null,
fields: RealmList<JavaField>? = null,
methods: RealmList<JavaMethod>? = null
): JavaClass {
return JavaClass().apply {
this.fqn = fqn
this.name = name
this.packageName = packageName
this.superClassFqn = superClassFqn
this.superInterfacesFqn = superInterfacesFqn
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.fields = fields
this.methods = methods
}
}
}
}
@@ -0,0 +1,114 @@
/*
* 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.models
import com.google.common.base.Objects
import io.realm.RealmAny
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
/**
* A constant value in Java.
*
* A unique ID is used as the primary key in order to ensure that a constant is not stored multiple
* times in the database. Also, a string constant always has type [JavaType.KIND_REF]. A constant
* is never of type [JavaType.KIND_VOID].
*
* @property id A unique ID which is generated for the constant.
* @property kind The kind of contant. See [JavaType] for a list of kinds.
* @property value The value of the constant. Cast this value based on the value of [kind].
* @author Akash Yadav
*/
@RealmClass
open class JavaConstant : ISharedJavaIndexable {
@PrimaryKey
@RealmField("id")
override var id: Int? = null
@RealmField("kind")
var kind: Short = JavaType.KIND_UNKNOWN
@RealmField("value")
var value: RealmAny? = null
companion object {
fun newInstance(kind: Short, value: RealmAny): JavaConstant {
return JavaConstant().apply {
this.kind = kind
this.value = value
this.computeId()
}
}
}
override fun computeId() {
this.id = Objects.hashCode(this.kind, this.kind, this.value)
}
// boolean, byte, short, int are stored as int in the bytecode
fun asBoolean(): Boolean? {
return safeCast(JavaType.KIND_BOOLEAN) { asInteger() == 1 }
}
fun asByte(): Byte? {
return safeCast(JavaType.KIND_BYTE) { asInteger()?.toByte() }
}
fun asShort(): Short? {
return safeCast(JavaType.KIND_SHORT) { asInteger()?.toShort() }
}
fun asInt(): Int? {
return safeCast(JavaType.KIND_INT) { asInteger() }
}
fun asLong(): Long? {
return safeCast(JavaType.KIND_LONG) { asLong() }
}
fun asFloat(): Float? {
return safeCast(JavaType.KIND_FLOAT) { asFloat() }
}
fun asDouble(): Double? {
return safeCast(JavaType.KIND_DOUBLE) { asDouble() }
}
fun asString(): String? {
return safeCast(JavaType.KIND_REF) { asString() }
}
fun asChar(): Char? {
return safeCast(JavaType.KIND_CHAR) { asInteger()?.toChar() }
}
private inline fun <R> safeCast(kind: Short, action: RealmAny.() -> R): R? {
return value?.let {
if (this.kind == kind) {
try {
it.action()
} catch (ex: ClassCastException) {
throw IllegalStateException("Kind == $kind but safeCast failed", ex)
}
} else null
}
}
}
@@ -0,0 +1,96 @@
/*
* 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.models
import io.realm.RealmList
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* A Java enum type.
*
* @author Akash Yadav
*/
@RealmClass
open class JavaEnum : IJavaType<JavaField, JavaMethod> {
@Index
@PrimaryKey
@Required
@RealmField("fqn")
override var fqn: String? = null
@Required
@RealmField("name")
override var name: String? = null
@Required
@RealmField("pck")
override var packageName: String? = null
@RealmField("superClassFqn")
override var superClassFqn: String? = null
@RealmField("superInterfacesFqn")
override var superInterfacesFqn: RealmList<String>? = null
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("fields")
override var fields: RealmList<JavaField>? = null
@RealmField("methods")
override var methods: RealmList<JavaMethod>? = null
override val isEnum: Boolean
get() = true
companion object {
@JvmStatic
fun newInstance(
fqn: String,
name: String,
packageName: String,
accessFlags: Int,
superClassFqn: String? = null,
superInterfacesFqn: RealmList<String>? = null,
apiInfo: ApiInfo? = null,
fields: RealmList<JavaField>? = null,
methods: RealmList<JavaMethod>? = null
): JavaEnum {
return JavaEnum().apply {
this.fqn = fqn
this.name = name
this.packageName = packageName
this.superClassFqn = superClassFqn
this.superInterfacesFqn = superInterfacesFqn
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.fields = fields
this.methods = methods
}
}
}
}
@@ -0,0 +1,69 @@
/*
* 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.models
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
/**
* @author Akash Yadav
*/
@RealmClass(embedded = true)
open class JavaField : IJavaSymbol {
@Index
@RealmField("name")
var name: String? = null
@RealmField("type")
var type: JavaType? = null
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("constantValue")
var constantValue: JavaConstant? = null
@LinkingObjects("fields")
val ofClass: RealmResults<JavaClass>? = null
companion object {
@JvmStatic
fun newField(
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
}
}
}
}
@@ -0,0 +1,95 @@
/*
* 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.models
import io.realm.RealmList
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* A Java interface type.
*
* @author Akash Yadav
*/
@RealmClass
open class JavaInterface : IJavaType<JavaField, JavaMethod> {
@Index
@PrimaryKey
@Required
@RealmField("fqn")
override var fqn: String? = null
@Required
@RealmField("name")
override var name: String? = null
@Required
@RealmField("pck")
override var packageName: String? = null
@RealmField("superClassFqn")
override var superClassFqn: String? = null
@RealmField("superInterfacesFqn")
override var superInterfacesFqn: RealmList<String>? = null
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@RealmField("fields")
override var fields: RealmList<JavaField>? = null
@RealmField("methods")
override var methods: RealmList<JavaMethod>? = null
override val isInterface: Boolean
get() = true
companion object {
@JvmStatic
fun newInstance(
fqn: String,
name: String,
packageName: String,
accessFlags: Int,
superInterfacesFqn: RealmList<String>? = null,
apiInfo: ApiInfo? = null,
fields: RealmList<JavaField>? = null,
methods: RealmList<JavaMethod>? = null
): JavaInterface {
return JavaInterface().apply {
this.fqn = fqn
this.name = name
this.packageName = packageName
this.superInterfacesFqn = superInterfacesFqn
this.accessFlags = accessFlags
this.apiInfo = apiInfo
this.fields = fields
this.methods = methods
}
}
}
}
@@ -0,0 +1,87 @@
/*
* 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.models
import io.realm.RealmList
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* @author Akash Yadav
*/
@RealmClass(embedded = true)
open class JavaMethod : IJavaSymbol {
@Index
@Required
@RealmField("name")
var name: String? = null
@RealmField("paramTypes")
var paramsTypes: RealmList<JavaType>? = null
@RealmField("returnType")
var returnType: JavaType? = null
@RealmField("accessFlags")
override var accessFlags: Int = 0
@RealmField("apiInfo")
override var apiInfo: ApiInfo? = null
@LinkingObjects("methods")
val ofClass: RealmResults<JavaClass>? = null
/**
* Returns the signature of the method.
*/
fun signature(): String {
val sb = StringBuilder()
sb.append(name)
sb.append("(")
paramsTypes?.forEach {
sb.append(it.internalForm())
}
sb.append(")")
sb.append(returnType?.internalForm())
return sb.toString()
}
companion object {
@JvmStatic
fun newInstance(
name: String,
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
}
}
}
}
@@ -0,0 +1,262 @@
/*
* 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.models
import com.google.common.base.Objects
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* @author Akash Yadav
*/
@RealmClass
open class JavaType : ISharedJavaIndexable {
@Required
@PrimaryKey
@RealmField("id")
override var id: Int? = null
@Index
@Required
@RealmField("name")
var name: String? = null
@RealmField("kind")
var kind: Short = KIND_UNKNOWN
@RealmField("arrayDims")
var arrayDims: Int = 0
/**
* Returns the internal representation of the type.
*/
fun internalForm(): String {
val sb = StringBuilder()
if (arrayDims > 0) {
sb.append("[".repeat(arrayDims))
}
if (kind == KIND_REF) {
sb.append("L")
}
sb.append(name)
if (kind == KIND_REF) {
sb.append(";")
}
return sb.toString()
}
/**
* Returns `true` if the type represents a primitive type.
*/
fun isPrimitive(): Boolean {
return when (kind) {
KIND_BOOLEAN,
KIND_BYTE,
KIND_CHAR,
KIND_DOUBLE,
KIND_FLOAT,
KIND_INT,
KIND_LONG,
KIND_SHORT -> true
else -> false
}
}
/**
* Returns `true` if the type represents a void type.
*/
fun isVoid(): Boolean {
return kind == KIND_VOID
}
/**
* Returns `true` if the type represents the `java.lang.String` type.
*/
fun isStringType(): Boolean {
return name == STRING.name
}
/**
* Returns `true` if the type represents the `java.lang.Object` type.
*/
fun isObjectType(): Boolean {
return name == OBJECT.name
}
/**
* Returns `true` if the type represents the `java.lang.Class` type.
*/
fun isClassType(): Boolean {
return name == CLASS.name
}
override fun computeId() {
this.id = Objects.hashCode(this.kind, this.name, this.arrayDims)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is JavaType) return false
if (name != other.name) return false
if (kind != other.kind) return false
if (arrayDims != other.arrayDims) return false
return true
}
override fun hashCode(): Int {
var result = name?.hashCode() ?: 0
result = 31 * result + kind
result = 31 * result + arrayDims
return result
}
override fun toString(): String {
return "JavaType(id=$id, name=$name, kind=$kind, arrayDims=$arrayDims)"
}
companion object {
const val KIND_UNKNOWN: Short = -1
const val KIND_BOOLEAN: Short = 0
const val KIND_BYTE: Short = 1
const val KIND_CHAR: Short = 2
const val KIND_DOUBLE: Short = 3
const val KIND_FLOAT: Short = 4
const val KIND_INT: Short = 5
const val KIND_LONG: Short = 6
const val KIND_SHORT: Short = 7
const val KIND_VOID: Short = 8
const val KIND_REF: Short = 9
const val TYPE_BOOLEAN = 'Z'
const val TYPE_BYTE = 'B'
const val TYPE_CHAR = 'C'
const val TYPE_DOUBLE = 'D'
const val TYPE_FLOAT = 'F'
const val TYPE_INT = 'I'
const val TYPE_LONG = 'J'
const val TYPE_SHORT = 'S'
const val TYPE_VOID = 'V'
@JvmStatic
val BOOLEAN = newInstance(name = TYPE_BOOLEAN, kind = KIND_INT)
@JvmStatic
val BYTE = newInstance(name = TYPE_BYTE, kind = KIND_BYTE)
@JvmStatic
val CHAR = newInstance(name = TYPE_CHAR, kind = KIND_CHAR)
@JvmStatic
val DOUBLE = newInstance(name = TYPE_DOUBLE, kind = KIND_DOUBLE)
@JvmStatic
val FLOAT = newInstance(name = TYPE_FLOAT, kind = KIND_FLOAT)
@JvmStatic
val INT = newInstance(name = TYPE_INT, kind = KIND_INT)
@JvmStatic
val LONG = newInstance(name = TYPE_LONG, kind = KIND_LONG)
@JvmStatic
val SHORT = newInstance(name = TYPE_SHORT, kind = KIND_SHORT)
@JvmStatic
val VOID = newInstance(name = TYPE_VOID, kind = KIND_VOID)
@JvmStatic
val STRING = newInstance(name = "java/lang/String", kind = KIND_REF)
@JvmStatic
val OBJECT = newInstance(name = "java/lang/Object", kind = KIND_REF)
@JvmStatic
val CLASS = newInstance(name = "java/lang/Class", kind = KIND_REF)
@JvmStatic
fun kindForType(type: Char) = when (type) {
TYPE_BOOLEAN -> KIND_BOOLEAN
TYPE_BYTE -> KIND_BYTE
TYPE_CHAR -> KIND_CHAR
TYPE_DOUBLE -> KIND_DOUBLE
TYPE_FLOAT -> KIND_FLOAT
TYPE_INT -> KIND_INT
TYPE_LONG -> KIND_LONG
TYPE_SHORT -> KIND_SHORT
TYPE_VOID -> KIND_VOID
else -> KIND_UNKNOWN
}
@JvmStatic
fun typeForKind(kind: Short) = when (kind) {
KIND_BOOLEAN -> TYPE_BOOLEAN
KIND_BYTE -> TYPE_BYTE
KIND_CHAR -> TYPE_CHAR
KIND_DOUBLE -> TYPE_DOUBLE
KIND_FLOAT -> TYPE_FLOAT
KIND_INT -> TYPE_INT
KIND_LONG -> TYPE_LONG
KIND_SHORT -> TYPE_SHORT
KIND_VOID -> TYPE_VOID
else -> TYPE_VOID
}
@JvmStatic
fun primitiveFor(char: Char): JavaType? {
return when (char) {
TYPE_BOOLEAN -> BOOLEAN
TYPE_BYTE -> BYTE
TYPE_CHAR -> CHAR
TYPE_DOUBLE -> DOUBLE
TYPE_FLOAT -> FLOAT
TYPE_INT -> INT
TYPE_LONG -> LONG
TYPE_SHORT -> SHORT
TYPE_VOID -> VOID
else -> null
}
}
@JvmStatic
fun newInstance(name: String?, kind: Short, arrayDims: Int = 0): JavaType {
return JavaType().apply {
this.name = name
this.kind = kind
this.arrayDims = arrayDims
this.computeId()
}
}
@JvmStatic
fun newInstance(name: Char, kind: Short, arrayDims: Int = 0): JavaType {
return newInstance(name.toString(), kind, arrayDims)
}
}
}
@@ -0,0 +1,220 @@
/*
* 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.models
import com.google.common.base.Objects
import io.realm.RealmAny
import io.realm.RealmDictionary
import io.realm.RealmList
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
sealed interface IAnnotationElementValue : ISharedJavaIndexable {
var kind: Byte
companion object {
const val KIND_UNKNOWN: Byte = -1
const val KIND_BOOLEAN: Byte = 0
const val KIND_BYTE: Byte = 1
const val KIND_CHAR: Byte = 2
const val KIND_DOUBLE: Byte = 3
const val KIND_FLOAT: Byte = 4
const val KIND_INT: Byte = 5
const val KIND_LONG: Byte = 6
const val KIND_SHORT: Byte = 7
const val KIND_STRING: Byte = 8
const val KIND_CLASS: Byte = 9
const val KIND_ENUM: Byte = 10
const val KIND_ANNOTATION: Byte = 11
const val KIND_ARRAY: Byte = 12
}
}
@RealmClass
open class PrimitiveAnnotationElementValue : IAnnotationElementValue {
@PrimaryKey
@Required
@RealmField("id")
override var id: Int? = null
@RealmField("kind")
override var kind: Byte = 0
@RealmField("value")
var value: JavaConstant? = null
override fun computeId() {
this.id = Objects.hashCode(this.kind, this.value)
}
companion object {
@JvmStatic
fun newInstance(kind: Byte, value: JavaConstant): PrimitiveAnnotationElementValue {
return PrimitiveAnnotationElementValue().apply {
this.kind = kind
this.value = value
this.computeId()
}
}
}
}
@RealmClass
open class EnumAnnotationElementValue : IAnnotationElementValue {
@PrimaryKey
@Required
@RealmField("id")
override var id: Int? = null
@RealmField("kind")
override var kind: Byte = IAnnotationElementValue.KIND_ENUM
@RealmField("name")
var name: String? = null
@RealmField("type")
var type: JavaType? = null
override fun computeId() {
this.id = Objects.hashCode(this.kind, this.name, this.type)
}
companion object {
@JvmStatic
fun newInstance(name: String, type: JavaType): EnumAnnotationElementValue {
return EnumAnnotationElementValue().apply {
this.name = name
this.type = type
this.computeId()
}
}
}
}
@RealmClass
open class ArrayAnnotationElementValue : IAnnotationElementValue {
@PrimaryKey
@Required
@RealmField("id")
override var id: Int? = null
@RealmField("kind")
override var kind: Byte = IAnnotationElementValue.KIND_ARRAY
@RealmField("numValues")
var numValues: Int = 0
/**
* The elements of the array element value. Always of type [IAnnotationElementValue].
*/
@RealmField("values")
var values: RealmList<RealmAny>? = null
override fun computeId() {
this.id = Objects.hashCode(this.kind, this.numValues, this.values)
}
companion object {
@JvmStatic
fun newInstance(values: RealmList<RealmAny>): ArrayAnnotationElementValue {
return ArrayAnnotationElementValue().apply {
this.numValues = values.size
this.values = values
this.computeId()
}
}
}
}
@RealmClass
open class ClassAnnotationElementValue : IAnnotationElementValue {
@PrimaryKey
@Required
@RealmField("id")
override var id: Int? = null
@RealmField("kind")
override var kind: Byte = IAnnotationElementValue.KIND_CLASS
@RealmField("type")
var type: JavaType? = null
override fun computeId() {
this.id = Objects.hashCode(this.kind, this.type)
}
companion object {
@JvmStatic
fun newInstance(type: JavaType): ClassAnnotationElementValue {
return ClassAnnotationElementValue().apply {
this.type = type
this.computeId()
}
}
}
}
@RealmClass
open class AnnotationAnnotationElementValue : IAnnotationElementValue {
@PrimaryKey
@Required
@RealmField("id")
override var id: Int? = null
@RealmField("kind")
override var kind: Byte = IAnnotationElementValue.KIND_ANNOTATION
@RealmField("type")
var type: JavaType? = null
/**
* The named elements of the annotation element value. Always of type [IAnnotationElementValue].
*/
@RealmField("values")
var values: RealmDictionary<RealmAny>? = null
override fun computeId() {
// RealmDictionary seems to not override hashCode()
this.id = Objects.hashCode(this.kind, this.type, this.values?.toMap()?.hashCode())
}
companion object {
@JvmStatic
fun newInstance(
type: JavaType,
values: RealmDictionary<RealmAny>
): AnnotationAnnotationElementValue {
return AnnotationAnnotationElementValue().apply {
this.type = type
this.values = values
this.computeId()
}
}
}
}
@@ -0,0 +1,62 @@
/*
* 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.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lsp.java.indexing.models.IJavaType
import com.itsaky.androidide.utils.FileProvider
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.io.File
/**
* @author Akash Yadav
*/
class AndroidJarIndexingRule : TestRule {
private val androidJar: File by lazy {
FileProvider.projectRoot()
.resolve("subprojects")
.resolve("framework-stubs")
.resolve("libs")
.resolve("android.jar")
.toFile()
}
private val androidJarIndexer: JavaIndexWorker by lazy {
assertThat(androidJar.exists()).isTrue()
JavaIndexWorker(androidJar)
}
val types: List<IJavaType<*, *>> by lazy {
androidJarIndexer.buildTypes()
}
override fun apply(base: Statement?, description: Description?): Statement {
// call 'types' to build the type models
types
base?.evaluate()
return object : Statement() {
override fun evaluate() {}
}
}
}
@@ -0,0 +1,120 @@
/*
* 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.lsp.java.indexing.models.JavaType
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
/**
* @author Akash Yadav
*/
@RunWith(RobolectricTestRunner::class)
class DescriptorUtilsTest {
@Test
fun testReturnType() {
val descriptor1 = "(I)V"
val expected1 = JavaType.newInstance(name = "V", kind = JavaType.KIND_VOID)
assertEquals(expected1, DescriptorUtils.returnType(descriptor1))
val descriptor2 = "(Ljava/lang/String;)I"
val expected2 = JavaType.newInstance(name = "I", kind = JavaType.KIND_INT)
assertEquals(expected2, DescriptorUtils.returnType(descriptor2))
val descriptor3 = "(I[Ljava/lang/String;)V"
val expected3 = JavaType.newInstance(name = "V", kind = JavaType.KIND_VOID)
assertEquals(expected3, DescriptorUtils.returnType(descriptor3))
}
@Test(expected = IllegalArgumentException::class)
fun testReturnTypeInvalidDescriptor() {
DescriptorUtils.returnType("Ljava/lang/String;I")
}
@Test
fun testParamTypes() {
val descriptor1 = "(I)V"
val expected1 = listOf(JavaType.newInstance(name = "I", kind = JavaType.KIND_INT))
assertEquals(expected1, DescriptorUtils.paramTypes(descriptor1))
val descriptor2 = "(Ljava/lang/String;I)V"
val expected2 = listOf(
JavaType.newInstance(name = "java/lang/String", kind = JavaType.KIND_REF),
JavaType.newInstance(name = "I", kind = JavaType.KIND_INT)
)
assertEquals(expected2, DescriptorUtils.paramTypes(descriptor2))
val descriptor3 = "([I[Ljava/lang/String;)V"
val expected3 = listOf(
JavaType.newInstance(name = "I", kind = JavaType.KIND_INT, arrayDims = 1),
JavaType.newInstance(name = "java/lang/String", kind = JavaType.KIND_REF, arrayDims = 1)
)
assertEquals(expected3, DescriptorUtils.paramTypes(descriptor3))
}
@Test(expected = IllegalArgumentException::class)
fun testParamTypesInvalidDescriptor() {
DescriptorUtils.paramTypes("Ljava/lang/String;I")
}
@Test
fun testPrimitive() {
val booleanType = JavaType.newInstance(name = "Z", kind = JavaType.KIND_BOOLEAN)
assertEquals(booleanType, DescriptorUtils.primitive('Z'))
val intType = JavaType.newInstance(name = "I", kind = JavaType.KIND_INT)
assertEquals(intType, DescriptorUtils.primitive('I'))
}
@Test(expected = IllegalArgumentException::class)
fun testPrimitiveInvalidType() {
DescriptorUtils.primitive('X')
}
@Test
fun testReferenceType() {
val descriptor = "Ljava/lang/String;"
val expected = JavaType.newInstance(name = "java/lang/String", kind = JavaType.KIND_REF)
assertEquals(expected, DescriptorUtils.referenceType(descriptor))
}
@Test(expected = IllegalArgumentException::class)
fun testReferenceTypeInvalidDescriptor() {
DescriptorUtils.referenceType("Ljava/lang/String")
}
@Test
fun testArrayType() {
val descriptor1 = "[I"
val expected1 = JavaType.newInstance(name = "I", kind = JavaType.KIND_INT, arrayDims = 1)
assertEquals(expected1, DescriptorUtils.arrayType(descriptor1))
val descriptor2 = "[[Ljava/lang/String;"
val expected2 =
JavaType.newInstance(name = "java/lang/String", kind = JavaType.KIND_REF, arrayDims = 2)
assertEquals(expected2, DescriptorUtils.arrayType(descriptor2))
}
@Test(expected = IllegalArgumentException::class)
fun testArrayTypeInvalidDescriptor() {
DescriptorUtils.arrayType("Ljava/lang/String;")
}
}
@@ -0,0 +1,475 @@
/*
* 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 android.view.View
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lsp.java.indexing.models.AnnotationElement
import com.itsaky.androidide.lsp.java.indexing.models.IAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.models.IJavaType
import com.itsaky.androidide.lsp.java.indexing.models.JavaClass
import com.itsaky.androidide.lsp.java.indexing.models.JavaEnum
import com.itsaky.androidide.lsp.java.indexing.models.JavaField
import com.itsaky.androidide.lsp.java.indexing.models.JavaMethod
import com.itsaky.androidide.lsp.java.indexing.models.JavaType
import com.itsaky.androidide.lsp.java.indexing.models.PrimitiveAnnotationElementValue
import io.realm.RealmAny
import openjdk.tools.classfile.AccessFlags
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.lang.reflect.Modifier
/**
* @author Akash Yadav
*/
@RunWith(RobolectricTestRunner::class)
class JavaIndexWorkerTest {
@Rule
@JvmField
val indexRule = AndroidJarIndexingRule()
private val types: List<IJavaType<*, *>>
get() = indexRule.types
@Test
fun `test JavaClass properties`() {
val sampleClass = types.firstOrNull { it.fqn == "java/lang/String" }
assertThat(sampleClass).isNotNull()
sampleClass?.let {
assertThat(it.fqn).isEqualTo("java/lang/String")
assertThat(it.name).isEqualTo("String")
assertThat(it.packageName).isEqualTo("java/lang")
assertThat(it.superClassFqn).isEqualTo("java/lang/Object")
assertThat(it.superInterfacesFqn).containsAtLeast(
"java/io/Serializable",
"java/lang/Comparable",
"java/lang/CharSequence"
)
}
}
@Test
fun `test JavaField properties`() {
val sampleClass = types.firstOrNull { it.fqn == "java/lang/String" }
val fields = sampleClass?.fields
assertThat(fields).isNotEmpty()
fields?.forEach { f ->
val field = f as JavaField
assertThat(field.name).isNotEmpty()
assertThat(field.accessFlags).isGreaterThan(0)
assertThat(field.type).isNotNull()
}
// java.lang.String in Android only has a single public field
assertThat(fields?.size).isEqualTo(1)
val field = fields?.firstOrNull() as JavaField?
assertThat(field).isNotNull()
assertThat(field?.name).isEqualTo("CASE_INSENSITIVE_ORDER")
assertThat(field?.type?.name).isEqualTo("java/util/Comparator")
assertThat(field?.type?.internalForm()).isEqualTo("Ljava/util/Comparator;")
assertThat(field?.type?.kind).isEqualTo(JavaType.KIND_REF)
assertThat(Modifier.isPublic(field?.accessFlags ?: 0)).isTrue()
assertThat(Modifier.isStatic(field?.accessFlags ?: 0)).isTrue()
assertThat(Modifier.isFinal(field?.accessFlags ?: 0)).isTrue()
}
@Test
fun `test JavaMethod properties`() {
val sampleClass = types.firstOrNull { it.fqn == "java/lang/String" }
val methods = sampleClass?.methods
assertThat(methods).isNotEmpty()
methods?.forEach { mth ->
val method = mth as JavaMethod
assertThat(method.name).isNotEmpty()
assertThat(method.accessFlags).isGreaterThan(0)
assertThat(method.paramsTypes).isNotNull()
assertThat(method.returnType).isNotNull()
}
assertThat(methods?.size).isAtLeast(1)
val method = (methods as List<JavaMethod>?)?.firstOrNull { it.name == "compareTo" }
assertThat(method).isNotNull()
assertThat(method?.name).isEqualTo("compareTo")
assertThat(method?.paramsTypes?.size).isEqualTo(1)
assertThat(method?.returnType?.name).isEqualTo("I")
assertThat(Modifier.isPublic(method?.accessFlags ?: 0)).isTrue()
assertThat(Modifier.isStatic(method?.accessFlags ?: 0)).isFalse()
assertThat(Modifier.isFinal(method?.accessFlags ?: 0)).isFalse()
val param = method?.paramsTypes?.firstOrNull()
assertThat(param).isNotNull()
assertThat(param?.name).isEqualTo("java/lang/String")
assertThat(param?.internalForm()).isEqualTo("Ljava/lang/String;")
assertThat(param?.kind).isEqualTo(JavaType.KIND_REF)
assertThat(param?.arrayDims).isEqualTo(0)
}
@Test
fun `test JavaType properties`() {
val sampleClass = types.firstOrNull { it.fqn == "java/lang/String" }
val fields = sampleClass?.fields
val methods = sampleClass?.methods
fields?.forEach { f ->
val field = f as JavaField
assertThat(field.type?.name).isNotEmpty()
assertThat(field.type?.kind).isNotEqualTo(JavaType.KIND_UNKNOWN)
}
methods?.forEach { mth ->
val method = mth as JavaMethod
method.paramsTypes?.forEach { paramType ->
assertThat(paramType.name).isNotEmpty()
assertThat(paramType.kind).isNotEqualTo(JavaType.KIND_UNKNOWN)
}
assertThat(method.returnType?.name).isNotEmpty()
assertThat(method.returnType?.kind).isNotEqualTo(JavaType.KIND_UNKNOWN)
}
}
@Test
fun `test integer constant field values`() {
val viewClass: IJavaType<JavaField, JavaMethod> =
types.first { it.fqn == "android/view/View" } as IJavaType<JavaField, JavaMethod>
assertThat(viewClass.fields).isNotEmpty()
val fields = viewClass.fields!!
val visible = fields.firstOrNull { it.name == "VISIBLE" }
assertThat(visible).isNotNull()
assertThat(visible?.type).isEqualTo(JavaType.INT)
val constantValue = visible?.constantValue
assertThat(constantValue).isNotNull()
assertThat(constantValue?.kind).isEqualTo(JavaType.KIND_INT)
assertThat(constantValue?.value?.type).isEqualTo(RealmAny.Type.INTEGER)
assertThat(constantValue?.asInt()).isNotNull()
assertThat(constantValue?.asInt()).isEqualTo(View.VISIBLE)
}
@Test
fun `test object type`() {
val obj = types.firstOrNull { it.fqn == "java/lang/Object" }
assertThat(obj).isNotNull()
assertThat(obj?.name).isEqualTo("Object")
assertThat(obj?.superClassFqn).isNull()
assertThat(obj?.superInterfacesFqn).isEmpty()
assertThat(obj?.fields).isEmpty()
assertThat(obj?.methods).isNotEmpty()
assertThat(obj?.isAnnotation).isFalse()
assertThat(obj?.isInterface).isFalse()
assertThat(obj?.isEnum).isFalse()
assertThat(obj?.isClass).isTrue()
assertThat(obj).isInstanceOf(JavaClass::class.java)
assertThat(obj?.packageName).isEqualTo("java/lang")
val access = AccessFlags(obj!!.accessFlags)
assertThat(access.`is`(AccessFlags.ACC_PUBLIC)).isTrue()
assertThat(access.`is`(AccessFlags.ACC_STATIC)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_INTERFACE)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_ANNOTATION)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_ENUM)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_ABSTRACT)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_FINAL)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_NATIVE)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_NATIVE)).isFalse()
}
@Test
fun `test class simple name`() {
val obj = types.firstOrNull { it.fqn == "android/view/View" }
assertThat(obj).isNotNull()
assertThat(obj?.name).isEqualTo("View")
assertThat(obj?.packageName).isEqualTo("android/view")
assertThat(obj?.superClassFqn).isEqualTo("java/lang/Object")
assertThat(obj?.superInterfacesFqn).isNotEmpty()
assertThat(obj?.fields).isNotEmpty()
assertThat(obj?.methods).isNotEmpty()
assertThat(obj?.isAnnotation).isFalse()
assertThat(obj?.isInterface).isFalse()
assertThat(obj?.isEnum).isFalse()
assertThat(obj?.isClass).isTrue()
assertThat(obj).isInstanceOf(JavaClass::class.java)
val access = AccessFlags(obj!!.accessFlags)
assertThat(access.`is`(AccessFlags.ACC_PUBLIC)).isTrue()
assertThat(access.`is`(AccessFlags.ACC_STATIC)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_INTERFACE)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_ANNOTATION)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_ENUM)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_ABSTRACT)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_FINAL)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_NATIVE)).isFalse()
assertThat(access.`is`(AccessFlags.ACC_NATIVE)).isFalse()
}
@Test
fun `test class superclass name`() {
types.firstOrNull { it.fqn == "android/view/View" }.also {
assertThat(it).isNotNull()
assertThat(it?.superClassFqn).isEqualTo("java/lang/Object")
}
}
@Test
fun `test class super interface names`() {
types.firstOrNull { it.fqn == "android/view/View" }.also {
assertThat(it).isNotNull()
assertThat(it?.superInterfacesFqn).containsExactly(
"android/graphics/drawable/Drawable\$Callback",
"android/view/KeyEvent\$Callback",
"android/view/accessibility/AccessibilityEventSource"
)
}
}
@Test
fun `test interface super interface names`() {
types.firstOrNull { it.fqn == "java/io/Closeable" }.also {
assertThat(it).isNotNull()
assertThat(it?.superInterfacesFqn).containsExactly("java/lang/AutoCloseable")
}
}
@Test
fun `test class fields`() {
types.firstOrNull { it.fqn == "android/view/View" }.also { view ->
assertThat(view).isNotNull()
assertThat(view?.fields?.map { field -> (field as JavaField).name }).containsAtLeast(
"VISIBLE",
"INVISIBLE",
"GONE",
"FOCUS_DOWN",
"FOCUS_UP",
"FOCUS_LEFT",
"FOCUS_RIGHT",
"FOCUS_BACKWARD",
"FOCUS_FORWARD",
)
}
types.firstOrNull { it.fqn == "android/view/ViewGroup\$LayoutParams" }.also { view ->
assertThat(view).isNotNull()
assertThat(view?.fields?.map { field -> (field as JavaField).name }).containsAtLeast(
"WRAP_CONTENT",
"MATCH_PARENT",
"FILL_PARENT",
)
}
}
@Test
fun `test interface fields`() {
types.firstOrNull { it.fqn == "android/provider/CalendarContract\$CalendarSyncColumns" }
.also { view ->
assertThat(view).isNotNull()
assertThat(view?.fields?.map { field -> (field as JavaField).name }).containsAtLeast(
"CAL_SYNC1",
"CAL_SYNC2",
"CAL_SYNC3",
"CAL_SYNC4",
"CAL_SYNC5",
"CAL_SYNC6",
"CAL_SYNC7",
"CAL_SYNC8",
"CAL_SYNC9",
"CAL_SYNC10",
)
}
}
@Test
fun `test mandatory enum fields`() {
types.filterIsInstance<JavaEnum>().forEach { enum ->
assertThat(enum.fields?.map { it.name }).contains("\$VALUES")
}
}
@Test
fun `test enum fields`() {
types.firstOrNull { it.fqn == "android/net/wifi/SupplicantState" }.also { enum ->
assertThat(enum?.fields?.map { (it as JavaField).name })
.containsAtLeast(
"DISCONNECTED",
"INTERFACE_DISABLED",
"INACTIVE",
"SCANNING",
"AUTHENTICATING",
"ASSOCIATING",
"ASSOCIATED",
"FOUR_WAY_HANDSHAKE",
"COMPLETED",
"GROUP_HANDSHAKE",
"DORMANT",
"UNINITIALIZED",
"INVALID"
)
}
}
@Test
fun `test annotation fields`() {
// android.jar doesn't seem to contain annotation classes with some fields
// TODO: add test for verifying how annotation fields are handled
}
@Test
fun `test class methods`() {
types.firstOrNull { it.fqn == "android/view/View" }.also { view ->
assertThat(view).isNotNull()
assertThat(view?.methods?.map { field -> (field as JavaMethod).signature() }).containsAtLeast(
"setId(I)V",
"getId()I",
"setOnClickListener(Landroid/view/View\$OnClickListener;)V",
"hasOnClickListeners()Z",
"hasOnLongClickListeners()Z",
"setAccessibilityDelegate(Landroid/view/View\$AccessibilityDelegate;)V",
"getAccessibilityDelegate()Landroid/view/View\$AccessibilityDelegate;",
"setAccessibilityLiveRegion(I)V",
"getAccessibilityLiveRegion()I",
"setAccessibilityHeading(Z)V",
"isAccessibilityHeading()Z",
"setPadding(IIII)V",
"setPaddingRelative(IIII)V",
)
}
}
@Test
fun `test interface methods`() {
types.firstOrNull { it.fqn == "android/view/View\$OnClickListener" }.also { view ->
assertThat(view).isNotNull()
assertThat(view?.methods?.map { field -> (field as JavaMethod).signature() })
.contains("onClick(Landroid/view/View;)V")
}
types.firstOrNull { it.fqn == "android/view/View\$OnLongClickListener" }.also { view ->
assertThat(view).isNotNull()
assertThat(view?.methods?.map { field -> (field as JavaMethod).signature() })
.containsAtLeast(
"onLongClick(Landroid/view/View;)Z",
"onLongClickUseDefaultHapticFeedback(Landroid/view/View;)Z"
)
}
}
@Test
fun `test mandatory enum methods`() {
types.filterIsInstance<JavaEnum>().forEach { enum ->
assertThat(enum.methods?.map { it.signature() }).containsAtLeast(
"values()[L${enum.fqn};",
"\$values()[L${enum.fqn};",
"valueOf(Ljava/lang/String;)L${enum.fqn};",
"<clinit>()V"
)
}
}
@Test
fun `test enum methods`() {
types.firstOrNull { it.fqn == "android/icu/text/DisplayContext" }.also { enum ->
assertThat(enum?.methods?.map { (it as JavaMethod).signature() }).contains(
"type()Landroid/icu/text/DisplayContext\$Type;"
)
}
}
@Test
fun `test annotation methods`() {
types.firstOrNull { it.fqn == "android/annotation/TargetApi" }.also { annotation ->
assertThat(annotation?.methods?.map { (it as AnnotationElement).signature() }).containsExactly(
"value()I"
)
}
}
@Test
fun `test annotation methods with default integer value`() {
types.firstOrNull { it.fqn == "java/lang/Deprecated" }.also { annotation ->
assertThat(annotation?.methods).isNotEmpty()
val since =
annotation?.methods?.find { (it as AnnotationElement).name == "since" } as AnnotationElement
assertThat(since.type).isEqualTo(JavaType.STRING)
var defValue = since.defaultValue
assertThat(defValue).isNotNull()
assertThat(defValue?.valueClass).isEqualTo(PrimitiveAnnotationElementValue::class.java)
var defVal = defValue?.asRealmModel(PrimitiveAnnotationElementValue::class.java)
assertThat(defVal).isNotNull()
assertThat(defVal?.kind).isEqualTo(IAnnotationElementValue.KIND_STRING)
assertThat(defVal?.value).isNotNull()
assertThat(defVal?.value?.kind).isEqualTo(JavaType.KIND_REF)
assertThat(defVal?.value?.asString()).isEqualTo("")
assertThat(defVal?.value?.asBoolean()).isNull()
assertThat(defVal?.value?.asByte()).isNull()
assertThat(defVal?.value?.asShort()).isNull()
assertThat(defVal?.value?.asChar()).isNull()
assertThat(defVal?.value?.asInt()).isNull()
assertThat(defVal?.value?.asLong()).isNull()
assertThat(defVal?.value?.asFloat()).isNull()
assertThat(defVal?.value?.asDouble()).isNull()
val forRemoval =
annotation.methods?.find { (it as AnnotationElement).name == "forRemoval" } as AnnotationElement
assertThat(forRemoval.type).isEqualTo(JavaType.BOOLEAN)
defValue = forRemoval.defaultValue
assertThat(defValue).isNotNull()
assertThat(defValue?.valueClass).isEqualTo(PrimitiveAnnotationElementValue::class.java)
defVal = defValue?.asRealmModel(PrimitiveAnnotationElementValue::class.java)
assertThat(defVal).isNotNull()
assertThat(defVal?.kind).isEqualTo(IAnnotationElementValue.KIND_BOOLEAN)
assertThat(defVal?.value).isNotNull()
assertThat(defVal?.value?.kind).isEqualTo(JavaType.KIND_BOOLEAN)
assertThat(defVal?.value?.asBoolean()).isEqualTo(false)
assertThat(defVal?.value?.asByte()).isNull()
assertThat(defVal?.value?.asShort()).isNull()
assertThat(defVal?.value?.asChar()).isNull()
assertThat(defVal?.value?.asInt()).isNull()
assertThat(defVal?.value?.asLong()).isNull()
assertThat(defVal?.value?.asFloat()).isNull()
assertThat(defVal?.value?.asDouble()).isNull()
assertThat(defVal?.value?.asString()).isNull()
}
}
@Test
fun `test annotation methods with array value`() {
types.firstOrNull { it.fqn == "java/lang/SuppressWarnings" }.also { annotation ->
assertThat(annotation?.methods).isNotEmpty()
val value =
annotation?.methods?.find { (it as AnnotationElement).name == "value" } as AnnotationElement
assertThat(value.type?.kind).isEqualTo(JavaType.KIND_REF)
assertThat(value.type?.name).isEqualTo("java/lang/String")
assertThat(value.type?.arrayDims).isEqualTo(1)
val defValue = value.defaultValue
assertThat(defValue).isNull()
}
}
}
@@ -22,7 +22,6 @@ import com.itsaky.androidide.lsp.api.LSPTest
import com.itsaky.androidide.projects.api.ModuleProject
import com.itsaky.androidide.projects.util.findAppModule
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
import com.itsaky.androidide.xml.versions.ApiVersions
import com.itsaky.androidide.xml.widgets.WidgetTable
import org.junit.Before
import org.junit.Ignore
+1
View File
@@ -140,6 +140,7 @@ include(
":subprojects:builder-model-impl",
":subprojects:flashbar",
":subprojects:framework-stubs",
":subprojects:indexing",
":subprojects:javac-services",
":subprojects:projects",
":subprojects:tooling-api",
@@ -0,0 +1,27 @@
/*
* 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.utils
import androidx.collection.mutableIntObjectMapOf
/**
* Create an IntIntMap from a vararg of pairs.
*/
fun intByteMapOf(vararg pairs: Pair<Int, Byte>) = mutableIntObjectMapOf<Byte>().apply {
pairs.forEach { pair -> put(pair.first, pair.second) }
}
+1
View File
@@ -0,0 +1 @@
/build
+38
View File
@@ -0,0 +1,38 @@
/*
* 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/>.
*/
import com.itsaky.androidide.build.config.BuildConfig
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-kapt")
id("realm-android")
}
android {
namespace = "${BuildConfig.packageName}.indexing"
}
dependencies {
kapt(libs.google.auto.service)
implementation(libs.google.auto.service.annotations)
api(projects.logger)
api(projects.common)
api(projects.shared)
}
@@ -0,0 +1,94 @@
/*
* 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.indexing
import com.itsaky.androidide.indexing.internal.DefaultIndex
/**
* An index of symbols of type [IndexableT].
*
* @author Akash Yadav
*/
interface IIndex<IndexableT : IIndexable> {
companion object {
private const val DEF_IS_ASYNC = true
/**
* Create an index path.
*/
fun createIndexPath(parentPath: String, name: String): String =
"${parentPath}${IRealmProvider.PATH_SEPARATOR}${name}"
}
/**
* The name of the index.
*/
val name: String
/**
* The path of the index. This does not represent the actual file path on the disk but rather used
* to identify the index.
*/
val path: String
/**
* Index the given symbol.
*
* @param symbol The symbol to index.
*/
fun index(symbol: IndexableT, async: Boolean = DEF_IS_ASYNC)
/**
* Index the given symbols.
*
* @param symbols The symbols to index.
*/
fun indexAll(symbols: Collection<IndexableT>, async: Boolean = DEF_IS_ASYNC) = indexBatched(
symbols,
symbols.size
)
/**
* Index the given symbols in batches.
*
* @param symbols The symbols to index.
* @param batchSize The number of symbols to index in each batch.
*/
fun indexBatched(
symbols: Collection<IndexableT>,
batchSize: Int = DefaultIndex.DEFAULT_PUT_BATCH_SIZE,
async: Boolean = DEF_IS_ASYNC,
)
/**
* Create a sub-index in this index.
*
* @param name The name of the sub-index.
* @return The created sub-index.
*/
fun <I : IIndexable> createSubIndex(
name: String,
config: IndexConfig? = null
): IIndex<I>
/**
* Delete the index.
*/
fun delete()
}
@@ -0,0 +1,28 @@
/*
* 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.indexing
import io.realm.RealmModel
/**
* An indexable object.
*
* @author Akash Yadav
*/
interface IIndexable : RealmModel {
}
@@ -0,0 +1,48 @@
/*
* 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.indexing
import kotlinx.coroutines.Deferred
/**
* An indexer is responsible for indexing symbols.
*
* @author Akash Yadav
*/
interface IIndexer<IndexableT : IIndexable, ReturnT> : AutoCloseable {
/**
* The path of the index. This does not represent the actual path in the file system, but rather the path
* that will be used to uniquely identify the index.
*/
val indexPath: String
/**
* Perform the indexing in the given index.
*
* @param index The index to perform the indexing in.
* @param params The parameters for the indexing.
* @return The collection of deferred results.
*/
fun index(index: IIndex<IndexableT>, params: Params): Collection<Deferred<ReturnT>>
/**
* The parameters for the indexer.
*/
interface Params
}
@@ -0,0 +1,69 @@
/*
* 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.indexing
import com.itsaky.androidide.utils.ServiceLoader
import io.realm.Realm
import io.realm.RealmConfiguration
/**
* An [IRealmProvider] provides the instance of [Realm].
*
* @author Akash Yadav
*/
interface IRealmProvider {
/**
* Get the [Realm] instance.
*
* @param path The path of the database.
*/
fun get(path: String): Realm {
return get(path, null)
}
/**
* Get the [Realm] instance.
*
* @param path The path of the database.
* @param config An optional function to configure the [RealmConfiguration] builder.
*/
fun get(path: String, config: (RealmConfiguration.Builder.() -> Unit)?): Realm
companion object {
private lateinit var sInstance: IRealmProvider
/**
* The separator used to separate segments in an index path.
*/
const val PATH_SEPARATOR = '/'
/**
* Get the [IRealmProvider] instance, finding the implementation if necessary.
*/
@JvmStatic
fun instance(): IRealmProvider {
if (!Companion::sInstance.isInitialized) {
val clazz = IRealmProvider::class.java
sInstance = ServiceLoader.load(clazz, clazz.classLoader).findFirstOrThrow()
}
return sInstance
}
}
}
@@ -0,0 +1,70 @@
/*
* 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.indexing
import com.itsaky.androidide.indexing.IRealmProvider.Companion.PATH_SEPARATOR
import com.itsaky.androidide.indexing.internal.DefaultIndex
import io.realm.RealmConfiguration
/**
* The [IndexProvider] provides APIs to index symbols in a database.
*
* @author itsaky
*/
object IndexProvider {
const val INDEX_BASE_PATH = "${PATH_SEPARATOR}indexes"
/**
* Get the index for the given indexable type in the root path.
*
* @param T The indexable type.
* @param config The index configuration.
* @param name The internal path of the index.
*/
fun <T : IIndexable> getRootIndex(
name: String,
config: IndexConfig? = null
): IIndex<T> {
require(name.indexOf(PATH_SEPARATOR) == -1)
return getIndexForPath("${PATH_SEPARATOR}${name}", config)
}
/**
* Get the index for the given indexable type.
*
* @param path The internal path of the index.
* @param config The index configuration.
*/
fun <T : IIndexable> getIndexForPath(
path: String,
config: IndexConfig? = null
): IIndex<T> {
val sepIdx = path.indexOfLast { it == PATH_SEPARATOR }
val name = if (sepIdx > 0) path.substring(sepIdx + 1) else path
val provider = IRealmProvider.instance()
val realm = provider.get("${INDEX_BASE_PATH}${path}", config)
val index = DefaultIndex.create<T>(name, path, realm)
return index
}
}
/**
* The [IndexConfig] is used to configure the index.
*/
typealias IndexConfig = RealmConfiguration.Builder.() -> Unit
@@ -0,0 +1,76 @@
/*
* 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.indexing.internal
import com.itsaky.androidide.indexing.IIndex
import com.itsaky.androidide.indexing.IIndexable
import com.itsaky.androidide.indexing.IndexConfig
import com.itsaky.androidide.indexing.IndexProvider
import io.realm.Realm
import org.slf4j.LoggerFactory
/**
* Default implementation of [IIndex].
*
* @author Akash Yadav
*/
class DefaultIndex<IndexableT : IIndexable> private constructor(
override val name: String,
override val path: String,
private val realm: Realm,
) : IIndex<IndexableT> {
companion object {
private val log = LoggerFactory.getLogger(DefaultIndex::class.java)
const val DEFAULT_PUT_BATCH_SIZE = 100
/**
* Creates a new [IIndex] instance.
*/
fun <IndexableT : IIndexable> create(
name: String,
path: String,
realm: Realm,
): IIndex<IndexableT> {
return DefaultIndex(name, path, realm)
}
}
// override fun query(condition: QueryCondition<IndexableT>): Query<IndexableT> {
// return box.query(condition).build()
// }
override fun index(symbol: IndexableT, async: Boolean) {
TODO("Not yet implemented")
}
override fun indexBatched(symbols: Collection<IndexableT>, batchSize: Int, async: Boolean) {
TODO("Not yet implemented")
}
override fun <I : IIndexable> createSubIndex(
name: String,
config: IndexConfig?
): IIndex<I> {
return IndexProvider.getIndexForPath(IIndex.createIndexPath(this.path, name), config)
}
override fun delete() {
TODO("Not yet implemented")
}
}
@@ -152,7 +152,7 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
dirs.addAll(mainSourceSet.sourceProvider.resDirectories!!)
}
val dependencies = getCompileModuleProjects().filterIsInstance(AndroidModule::class.java)
val dependencies = getCompileModuleProjects().filterIsInstance<AndroidModule>()
for (dependency in dependencies) {
dirs.addAll(dependency.getResourceDirectories())
+15 -3
View File
@@ -26,13 +26,25 @@ android {
namespace = "${BuildConfig.packageName}.testing.android"
}
configurations.api {
// There is a transitive dependency on 'protobuf-lite' in 'androidTest' configurations
// which conflicts with the 'protobuf-javalite' library we use
// hence we exclude the dependency and add ours manually
//+--- androidx.test.espresso:espresso-core:<...>
// +--- com.google.android.material:material:<...>
// \--- com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:<...>
// +--- com.google.protobuf:protobuf-lite:3.0.1
exclude(group = "com.google.protobuf", module = "protobuf-lite")
}
dependencies {
api(libs.google.protobuf)
api(libs.tests.androidx.espresso.core)
api(libs.tests.androidx.espresso.contrib)
api(libs.tests.androidx.junit)
api(libs.tests.androidx.test.core)
api(libs.tests.androidx.test.runner)
api(libs.tests.androidx.test.rules)
api(libs.tests.androidx.junit)
api(libs.tests.androidx.espresso.core)
api(libs.tests.androidx.espresso.contrib)
api(libs.tests.androidx.uiautomator)
api(libs.tests.junit)
api(libs.tests.google.truth)
+15 -13
View File
@@ -18,25 +18,27 @@
import com.itsaky.androidide.build.config.BuildConfig
plugins {
id("com.android.library")
id("kotlin-android")
id("com.android.library")
id("kotlin-android")
}
android {
namespace = "${BuildConfig.packageName}.lsp.testing"
namespace = "${BuildConfig.packageName}.lsp.testing"
}
dependencies {
api(libs.androidx.core.ktx)
api(libs.common.editor)
api(libs.common.kotlin)
api(libs.androidx.core.ktx)
api(libs.common.editor)
api(libs.common.kotlin)
api(projects.actions)
api(projects.common)
api(projects.eventbusAndroid)
api(projects.lsp.api)
api(libs.tests.mockito.kotlin)
api(projects.testing.common)
api(projects.testing.tooling)
api(projects.testing.unit)
api(projects.actions)
api(projects.common)
api(projects.eventbusAndroid)
api(projects.lsp.api)
api(projects.testing.common)
api(projects.testing.tooling)
api(projects.testing.unit)
}