diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0379cfbd3..62e591b2a 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt index 08ef42188..a2138a485 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt @@ -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 diff --git a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt index bac4b5d61..1039b9701 100755 --- a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt +++ b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt @@ -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(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 { 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) diff --git a/app/src/main/java/com/itsaky/androidide/indexing/DatabaseEntity.kt b/app/src/main/java/com/itsaky/androidide/indexing/DatabaseEntity.kt new file mode 100644 index 000000000..41a264e24 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/indexing/DatabaseEntity.kt @@ -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 . + */ + +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() \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/indexing/RealmProviderImpl.kt b/app/src/main/java/com/itsaky/androidide/indexing/RealmProviderImpl.kt new file mode 100644 index 000000000..0ab5053a2 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/indexing/RealmProviderImpl.kt @@ -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 . + */ + +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 { + 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 + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 7c082ab8f..731884cec 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,7 @@ buildscript { dependencies { classpath(libs.kotlin.gradle.plugin) classpath(libs.nav.safe.args.gradle.plugin) + classpath(libs.realm.gradle.plugin) } } diff --git a/common/src/main/java/com/itsaky/androidide/utils/Environment.java b/common/src/main/java/com/itsaky/androidide/utils/Environment.java index 71121f1d3..7d7574c55 100755 --- a/common/src/main/java/com/itsaky/androidide/utils/Environment.java +++ b/common/src/main/java/com/itsaky/androidide/utils/Environment.java @@ -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"); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 46f2a34b1..0ca6e1e14 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/lsp/java/build.gradle.kts b/lsp/java/build.gradle.kts index 5ca25693b..a90023832 100644 --- a/lsp/java/build.gradle.kts +++ b/lsp/java/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt new file mode 100644 index 000000000..2e9c92528 --- /dev/null +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt @@ -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 . + */ + +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, MutableList>() + + 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() + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/DescriptorUtils.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/DescriptorUtils.kt new file mode 100644 index 000000000..e51bd33b4 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/DescriptorUtils.kt @@ -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 . + */ + +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 { + val idx = descriptor.indexOfLast { it == ')' } + if (descriptor[0] != '(' || idx == -1) { + throw IllegalArgumentException("Invalid method descriptor: $descriptor") + } + + val types = mutableListOf() + 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) + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorker.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorker.kt new file mode 100644 index 000000000..da39b7833 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorker.kt @@ -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 . + */ + +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> { + val types = mutableListOf>() + 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() + 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().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().apply { + addAll(DescriptorUtils.paramTypes(descriptor)) + } + + JavaMethod.newInstance(methodName, paramTypes, methodType, method.access_flags.flags) + }) + } + } + + private fun IJavaType.setFields(classFile: ClassFile) { + fields = RealmList().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().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().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().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) + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt new file mode 100644 index 000000000..36e0a174a --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt @@ -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 . + */ + +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 +) : IIndexer { + + companion object { + const val INDEX_PATH = "/java" + } + + private val coroutineScope = CoroutineScope(Dispatchers.IO) + override val indexPath: String = INDEX_PATH + + override fun index(index: IIndex, params: Params): Collection> { + return jarPaths.map { jar -> + coroutineScope.async { + val worker = JavaIndexWorker(jar) +// worker.index(index) + } + } + } + + override fun close() { + coroutineScope.cancelIfActive("Cancellation requested") + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/AnnotationElement.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/AnnotationElement.kt new file mode 100644 index 000000000..164490a46 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/AnnotationElement.kt @@ -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 . + */ + +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? = 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 + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ApiInfo.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ApiInfo.kt new file mode 100644 index 000000000..98a7b4eae --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ApiInfo.kt @@ -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 . + */ + +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() + } + } + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaIndexable.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaIndexable.kt new file mode 100644 index 000000000..bced5daeb --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaIndexable.kt @@ -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 . + */ + +package com.itsaky.androidide.lsp.java.indexing.models + +import com.itsaky.androidide.indexing.IIndexable + +/** + * @author Akash Yadav + */ +interface IJavaIndexable : IIndexable \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaSymbol.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaSymbol.kt new file mode 100644 index 000000000..d6e93b484 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaSymbol.kt @@ -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 . + */ + +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? +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaType.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaType.kt new file mode 100644 index 000000000..d45d35801 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/IJavaType.kt @@ -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 . + */ + +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 : 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? + var fields: RealmList? + var methods: RealmList? + + val isClass: Boolean + get() = false + + val isInterface: Boolean + get() = false + + val isAnnotation: Boolean + get() = false + + val isEnum: Boolean + get() = false +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ISharedJavaIndexable.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ISharedJavaIndexable.kt new file mode 100644 index 000000000..ec5d37652 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ISharedJavaIndexable.kt @@ -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 . + */ + +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() +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaAnnotation.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaAnnotation.kt new file mode 100644 index 000000000..92512ee50 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaAnnotation.kt @@ -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 . + */ + +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 { + + @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? = null + + @RealmField("accessFlags") + override var accessFlags: Int = 0 + + @RealmField("apiInfo") + override var apiInfo: ApiInfo? = null + + @RealmField("fields") + override var fields: RealmList? = null + + @RealmField("methods") + override var methods: RealmList? = 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? = null, + apiInfo: ApiInfo? = null, + fields: RealmList? = null, + methods: RealmList? = 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 + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaClass.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaClass.kt new file mode 100644 index 000000000..9ca2d68e4 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaClass.kt @@ -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 . + */ + +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 { + + @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? = null + + @RealmField("accessFlags") + override var accessFlags: Int = 0 + + @RealmField("apiInfo") + override var apiInfo: ApiInfo? = null + + @RealmField("fields") + override var fields: RealmList? = null + + @RealmField("methods") + override var methods: RealmList? = 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? = null, + apiInfo: ApiInfo? = null, + fields: RealmList? = null, + methods: RealmList? = 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 + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaConstant.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaConstant.kt new file mode 100644 index 000000000..b00017ab7 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaConstant.kt @@ -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 . + */ + +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 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 + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaEnum.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaEnum.kt new file mode 100644 index 000000000..f9a1fd666 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaEnum.kt @@ -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 . + */ + +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 { + + @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? = null + + @RealmField("accessFlags") + override var accessFlags: Int = 0 + + @RealmField("apiInfo") + override var apiInfo: ApiInfo? = null + + @RealmField("fields") + override var fields: RealmList? = null + + @RealmField("methods") + override var methods: RealmList? = 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? = null, + apiInfo: ApiInfo? = null, + fields: RealmList? = null, + methods: RealmList? = 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 + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaField.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaField.kt new file mode 100644 index 000000000..dc4068f7f --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaField.kt @@ -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 . + */ + +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? = 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 + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaInterface.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaInterface.kt new file mode 100644 index 000000000..e7e46a92c --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaInterface.kt @@ -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 . + */ + +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 { + + @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? = null + + @RealmField("accessFlags") + override var accessFlags: Int = 0 + + @RealmField("apiInfo") + override var apiInfo: ApiInfo? = null + + @RealmField("fields") + override var fields: RealmList? = null + + @RealmField("methods") + override var methods: RealmList? = null + + override val isInterface: Boolean + get() = true + + companion object { + @JvmStatic + fun newInstance( + fqn: String, + name: String, + packageName: String, + accessFlags: Int, + superInterfacesFqn: RealmList? = null, + apiInfo: ApiInfo? = null, + fields: RealmList? = null, + methods: RealmList? = 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 + } + } + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaMethod.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaMethod.kt new file mode 100644 index 000000000..117743b50 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaMethod.kt @@ -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 . + */ + +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? = 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? = 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, + 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 + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaType.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaType.kt new file mode 100644 index 000000000..fb1d3b033 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaType.kt @@ -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 . + */ + +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) + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/annotationElementValues.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/annotationElementValues.kt new file mode 100644 index 000000000..d80fc5b68 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/annotationElementValues.kt @@ -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 . + */ + +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? = null + + override fun computeId() { + this.id = Objects.hashCode(this.kind, this.numValues, this.values) + } + + companion object { + @JvmStatic + fun newInstance(values: RealmList): 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? = 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 + ): AnnotationAnnotationElementValue { + return AnnotationAnnotationElementValue().apply { + this.type = type + this.values = values + this.computeId() + } + } + } +} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarIndexingRule.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarIndexingRule.kt new file mode 100644 index 000000000..1ce770605 --- /dev/null +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarIndexingRule.kt @@ -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 . + */ + +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> 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() {} + } + } +} \ No newline at end of file diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/DescriptorUtilsTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/DescriptorUtilsTest.kt new file mode 100644 index 000000000..447061466 --- /dev/null +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/DescriptorUtilsTest.kt @@ -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 . + */ + +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;") + } +} \ No newline at end of file diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt new file mode 100644 index 000000000..41b12f0c2 --- /dev/null +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt @@ -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 . + */ + +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> + 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?)?.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 = + types.first { it.fqn == "android/view/View" } as IJavaType + 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().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().forEach { enum -> + assertThat(enum.methods?.map { it.signature() }).containsAtLeast( + "values()[L${enum.fqn};", + "\$values()[L${enum.fqn};", + "valueOf(Ljava/lang/String;)L${enum.fqn};", + "()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() + } + } +} diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt index 89ef6b248..6d9bc0691 100644 --- a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts index 47d0c2d9f..831bb3f51 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -140,6 +140,7 @@ include( ":subprojects:builder-model-impl", ":subprojects:flashbar", ":subprojects:framework-stubs", + ":subprojects:indexing", ":subprojects:javac-services", ":subprojects:projects", ":subprojects:tooling-api", diff --git a/shared/src/main/java/com/itsaky/androidide/utils/collectionExts.kt b/shared/src/main/java/com/itsaky/androidide/utils/collectionExts.kt new file mode 100644 index 000000000..9709388b1 --- /dev/null +++ b/shared/src/main/java/com/itsaky/androidide/utils/collectionExts.kt @@ -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 . + */ + +package com.itsaky.androidide.utils + +import androidx.collection.mutableIntObjectMapOf + +/** + * Create an IntIntMap from a vararg of pairs. + */ +fun intByteMapOf(vararg pairs: Pair) = mutableIntObjectMapOf().apply { + pairs.forEach { pair -> put(pair.first, pair.second) } +} \ No newline at end of file diff --git a/subprojects/indexing/.gitignore b/subprojects/indexing/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/subprojects/indexing/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/subprojects/indexing/build.gradle.kts b/subprojects/indexing/build.gradle.kts new file mode 100644 index 000000000..f02a6b14a --- /dev/null +++ b/subprojects/indexing/build.gradle.kts @@ -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 . + */ + +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) +} \ No newline at end of file diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndex.kt b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndex.kt new file mode 100644 index 000000000..4b983653e --- /dev/null +++ b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndex.kt @@ -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 . + */ + +package com.itsaky.androidide.indexing + +import com.itsaky.androidide.indexing.internal.DefaultIndex + +/** + * An index of symbols of type [IndexableT]. + * + * @author Akash Yadav + */ +interface IIndex { + + 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, 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, + 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 createSubIndex( + name: String, + config: IndexConfig? = null + ): IIndex + + /** + * Delete the index. + */ + fun delete() +} \ No newline at end of file diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndexable.kt b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndexable.kt new file mode 100644 index 000000000..bcdeed08e --- /dev/null +++ b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndexable.kt @@ -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 . + */ + +package com.itsaky.androidide.indexing + +import io.realm.RealmModel + +/** + * An indexable object. + * + * @author Akash Yadav + */ +interface IIndexable : RealmModel { +} \ No newline at end of file diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndexer.kt b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndexer.kt new file mode 100644 index 000000000..150af9906 --- /dev/null +++ b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndexer.kt @@ -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 . + */ + +package com.itsaky.androidide.indexing + +import kotlinx.coroutines.Deferred + +/** + * An indexer is responsible for indexing symbols. + * + * @author Akash Yadav + */ +interface IIndexer : 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, params: Params): Collection> + + /** + * The parameters for the indexer. + */ + interface Params +} \ No newline at end of file diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IRealmProvider.kt b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IRealmProvider.kt new file mode 100644 index 000000000..f347b3554 --- /dev/null +++ b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IRealmProvider.kt @@ -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 . + */ + +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 + } + } +} \ No newline at end of file diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IndexProvider.kt b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IndexProvider.kt new file mode 100644 index 000000000..5590c9fda --- /dev/null +++ b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IndexProvider.kt @@ -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 . + */ + +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 getRootIndex( + name: String, + config: IndexConfig? = null + ): IIndex { + 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 getIndexForPath( + path: String, + config: IndexConfig? = null + ): IIndex { + 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(name, path, realm) + return index + } +} + +/** + * The [IndexConfig] is used to configure the index. + */ +typealias IndexConfig = RealmConfiguration.Builder.() -> Unit diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/internal/DefaultIndex.kt b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/internal/DefaultIndex.kt new file mode 100644 index 000000000..eef4333c0 --- /dev/null +++ b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/internal/DefaultIndex.kt @@ -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 . + */ + +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 private constructor( + override val name: String, + override val path: String, + private val realm: Realm, +) : IIndex { + + companion object { + private val log = LoggerFactory.getLogger(DefaultIndex::class.java) + const val DEFAULT_PUT_BATCH_SIZE = 100 + + /** + * Creates a new [IIndex] instance. + */ + fun create( + name: String, + path: String, + realm: Realm, + ): IIndex { + return DefaultIndex(name, path, realm) + } + } + +// override fun query(condition: QueryCondition): Query { +// return box.query(condition).build() +// } + + override fun index(symbol: IndexableT, async: Boolean) { + TODO("Not yet implemented") + } + + override fun indexBatched(symbols: Collection, batchSize: Int, async: Boolean) { + TODO("Not yet implemented") + } + + override fun createSubIndex( + name: String, + config: IndexConfig? + ): IIndex { + return IndexProvider.getIndexForPath(IIndex.createIndexPath(this.path, name), config) + } + + override fun delete() { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt index 06973daa5..ca0c29d4b 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt @@ -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() for (dependency in dependencies) { dirs.addAll(dependency.getResourceDirectories()) diff --git a/testing/android/build.gradle.kts b/testing/android/build.gradle.kts index 123180f8e..017429b4d 100644 --- a/testing/android/build.gradle.kts +++ b/testing/android/build.gradle.kts @@ -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) diff --git a/testing/lsp/build.gradle.kts b/testing/lsp/build.gradle.kts index 2bd3f2929..18e4cb012 100644 --- a/testing/lsp/build.gradle.kts +++ b/testing/lsp/build.gradle.kts @@ -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) } \ No newline at end of file