-
Notifications
You must be signed in to change notification settings - Fork 245
Setup incremental indexing on file changes #323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,79 @@ | ||
| package org.javacs.kt.index | ||
|
|
||
| import org.jetbrains.exposed.sql.and | ||
| import org.jetbrains.exposed.sql.count | ||
| import org.jetbrains.exposed.sql.deleteAll | ||
| import org.jetbrains.exposed.sql.innerJoin | ||
| import org.jetbrains.exposed.sql.replace | ||
| import org.jetbrains.exposed.sql.select | ||
| import org.jetbrains.exposed.sql.selectAll | ||
| import org.jetbrains.exposed.sql.Table | ||
| import org.jetbrains.exposed.sql.transactions.transaction | ||
| import org.jetbrains.exposed.sql.SchemaUtils | ||
| import org.jetbrains.kotlin.descriptors.ModuleDescriptor | ||
| import org.jetbrains.kotlin.descriptors.DeclarationDescriptor | ||
| import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter | ||
| import org.jetbrains.kotlin.resolve.scopes.MemberScope | ||
| import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe | ||
| import org.jetbrains.kotlin.name.FqName | ||
| import org.jetbrains.kotlin.psi2ir.intermediate.extensionReceiverType | ||
| import org.javacs.kt.LOG | ||
| import org.javacs.kt.progress.Progress | ||
| import org.jetbrains.exposed.sql.Database | ||
| import org.jetbrains.exposed.sql.SqlExpressionBuilder.like | ||
| import org.jetbrains.exposed.sql.insert | ||
|
|
||
| private val MAX_FQNAME_LENGTH = 255 | ||
| private val MAX_SHORT_NAME_LENGTH = 80 | ||
|
|
||
| private object Symbols : Table() { | ||
| val fqName = varchar("fqname", length = MAX_FQNAME_LENGTH) references FqNames.fqName | ||
| import org.jetbrains.exposed.dao.IntEntity | ||
| import org.jetbrains.exposed.dao.IntEntityClass | ||
| import org.jetbrains.exposed.dao.id.EntityID | ||
| import org.jetbrains.exposed.dao.id.IntIdTable | ||
| import org.jetbrains.exposed.sql.* | ||
| import java.util.concurrent.CompletableFuture | ||
| import kotlin.sequences.Sequence | ||
|
|
||
| private const val MAX_FQNAME_LENGTH = 255 | ||
| private const val MAX_SHORT_NAME_LENGTH = 80 | ||
| private const val MAX_URI_LENGTH = 511 | ||
|
|
||
| private object Symbols : IntIdTable() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to understand, we are now using integers (instead of fully-qualified names) to identify symbols. Can we still be sure that uniqueness is maintained (perhaps by using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I didn't use a unique index because I think it would complicate things when a user creates multiple classes with the same fully qualified name. Even though it's illegal (and will lead to compilation errors), it's still technically possible for the user to do this. Therefore, I chose to include the duplicate entries in the index. We could certainly make it unique, but I think we would need to add some more logic to prevent duplicates. And if we ever start using the index for other things (like definitions, for example) duplicated symbols would be ignored from that. |
||
| val fqName = varchar("fqname", length = MAX_FQNAME_LENGTH).index() | ||
| val shortName = varchar("shortname", length = MAX_SHORT_NAME_LENGTH) | ||
| val kind = integer("kind") | ||
| val visibility = integer("visibility") | ||
| val extensionReceiverType = varchar("extensionreceivertype", length = MAX_FQNAME_LENGTH).nullable() | ||
| val location = optReference("location", Locations) | ||
| } | ||
|
|
||
| override val primaryKey = PrimaryKey(fqName) | ||
| private object Locations : IntIdTable() { | ||
| val uri = varchar("uri", length = MAX_URI_LENGTH) | ||
| val range = reference("range", Ranges) | ||
| } | ||
|
|
||
| private object FqNames : Table() { | ||
| val fqName = varchar("fqname", length = MAX_FQNAME_LENGTH) | ||
| val shortName = varchar("shortname", length = MAX_SHORT_NAME_LENGTH) | ||
| private object Ranges : IntIdTable() { | ||
| val start = reference("start", Positions) | ||
| val end = reference("end", Positions) | ||
| } | ||
|
|
||
| private object Positions : IntIdTable() { | ||
| val line = integer("line") | ||
| val character = integer("character") | ||
| } | ||
|
|
||
| class SymbolEntity(id: EntityID<Int>) : IntEntity(id) { | ||
| companion object : IntEntityClass<SymbolEntity>(Symbols) | ||
|
|
||
| var fqName by Symbols.fqName | ||
| var shortName by Symbols.shortName | ||
| var kind by Symbols.kind | ||
| var visibility by Symbols.visibility | ||
| var extensionReceiverType by Symbols.extensionReceiverType | ||
| var location by LocationEntity optionalReferencedOn Symbols.location | ||
| } | ||
|
|
||
| class LocationEntity(id: EntityID<Int>) : IntEntity(id) { | ||
| companion object : IntEntityClass<LocationEntity>(Locations) | ||
|
|
||
| override val primaryKey = PrimaryKey(fqName) | ||
| var uri by Locations.uri | ||
| var range by RangeEntity referencedOn Locations.range | ||
| } | ||
|
|
||
| class RangeEntity(id: EntityID<Int>) : IntEntity(id) { | ||
| companion object : IntEntityClass<RangeEntity>(Ranges) | ||
|
|
||
| var start by PositionEntity referencedOn Ranges.start | ||
| var end by PositionEntity referencedOn Ranges.end | ||
| } | ||
|
|
||
| class PositionEntity(id: EntityID<Int>) : IntEntity(id) { | ||
| companion object : IntEntityClass<PositionEntity>(Positions) | ||
|
|
||
| var line by Positions.line | ||
| var character by Positions.character | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -48,47 +82,49 @@ private object FqNames : Table() { | |
| class SymbolIndex { | ||
| private val db = Database.connect("jdbc:h2:mem:symbolindex;DB_CLOSE_DELAY=-1", "org.h2.Driver") | ||
|
|
||
| var progressFactory: Progress.Factory = Progress.Factory.None | ||
| var progressFactory: Progress.Factory = object: Progress.Factory { | ||
| override fun create(label: String): CompletableFuture<Progress> = CompletableFuture.supplyAsync { Progress.None } | ||
| } | ||
|
||
|
|
||
| init { | ||
| transaction(db) { | ||
| SchemaUtils.create(Symbols, FqNames) | ||
| SchemaUtils.create(Symbols, Locations, Ranges, Positions) | ||
| } | ||
| } | ||
|
|
||
| /** Rebuilds the entire index. May take a while. */ | ||
| fun refresh(module: ModuleDescriptor) { | ||
| fun refresh(module: ModuleDescriptor, exclusions: Sequence<DeclarationDescriptor>) { | ||
| val started = System.currentTimeMillis() | ||
| LOG.info("Updating full symbol index...") | ||
|
|
||
| progressFactory.create("Indexing").thenApplyAsync { progress -> | ||
| try { | ||
| transaction(db) { | ||
| addDeclarations(allDescriptors(module, exclusions)) | ||
|
|
||
| val finished = System.currentTimeMillis() | ||
| val count = Symbols.slice(Symbols.fqName.count()).selectAll().first()[Symbols.fqName.count()] | ||
| LOG.info("Updated full symbol index in ${finished - started} ms! (${count} symbol(s))") | ||
| } | ||
| } catch (e: Exception) { | ||
| LOG.error("Error while updating symbol index") | ||
| LOG.printStackTrace(e) | ||
| } | ||
|
|
||
| progress.close() | ||
| } | ||
| } | ||
|
|
||
| // Removes a list of indexes and adds another list. Everything is done in the same transaction. | ||
| fun updateIndexes(remove: Sequence<DeclarationDescriptor>, add: Sequence<DeclarationDescriptor>) { | ||
| val started = System.currentTimeMillis() | ||
| LOG.info("Updating symbol index...") | ||
|
|
||
| progressFactory.create("Indexing").thenApply { progress -> | ||
| progressFactory.create("Indexing").thenApplyAsync { progress -> | ||
| try { | ||
| // TODO: Incremental updates | ||
| transaction(db) { | ||
| Symbols.deleteAll() | ||
|
|
||
| for (descriptor in allDescriptors(module)) { | ||
| val descriptorFqn = descriptor.fqNameSafe | ||
| val extensionReceiverFqn = descriptor.accept(ExtractSymbolExtensionReceiverType, Unit)?.takeIf { !it.isRoot } | ||
|
|
||
| if (canStoreFqName(descriptorFqn) && (extensionReceiverFqn?.let { canStoreFqName(it) } ?: true)) { | ||
| for (fqn in listOf(descriptorFqn, extensionReceiverFqn).filterNotNull()) { | ||
| FqNames.replace { | ||
| it[fqName] = fqn.toString() | ||
| it[shortName] = fqn.shortName().toString() | ||
| } | ||
| } | ||
|
|
||
| Symbols.replace { | ||
| it[fqName] = descriptorFqn.toString() | ||
| it[kind] = descriptor.accept(ExtractSymbolKind, Unit).rawValue | ||
| it[visibility] = descriptor.accept(ExtractSymbolVisibility, Unit).rawValue | ||
| it[extensionReceiverType] = extensionReceiverFqn?.toString() | ||
| } | ||
| } else { | ||
| LOG.warn("Excluding symbol {} from index since its name is too long", descriptorFqn.toString()) | ||
| } | ||
| } | ||
| removeDeclarations(remove) | ||
| addDeclarations(add) | ||
|
|
||
| val finished = System.currentTimeMillis() | ||
| val count = Symbols.slice(Symbols.fqName.count()).selectAll().first()[Symbols.fqName.count()] | ||
|
|
@@ -103,29 +139,68 @@ class SymbolIndex { | |
| } | ||
| } | ||
|
|
||
| private fun canStoreFqName(fqName: FqName) = | ||
| fqName.toString().length <= MAX_FQNAME_LENGTH | ||
| && fqName.shortName().toString().length <= MAX_SHORT_NAME_LENGTH | ||
| private fun Transaction.removeDeclarations(declarations: Sequence<DeclarationDescriptor>) = | ||
| declarations.forEach { declaration -> | ||
| val (descriptorFqn, extensionReceiverFqn) = getFqNames(declaration) | ||
|
|
||
| if (validFqName(descriptorFqn) && (extensionReceiverFqn?.let { validFqName(it) } != false)) { | ||
| Symbols.deleteWhere { | ||
| (Symbols.fqName eq descriptorFqn.toString()) and (Symbols.extensionReceiverType eq extensionReceiverFqn?.toString()) | ||
| } | ||
| } else { | ||
| LOG.warn("Excluding symbol {} from index since its name is too long", descriptorFqn.toString()) | ||
| } | ||
| } | ||
|
|
||
| private fun Transaction.addDeclarations(declarations: Sequence<DeclarationDescriptor>) = | ||
|
||
| declarations.forEach { declaration -> | ||
| val (descriptorFqn, extensionReceiverFqn) = getFqNames(declaration) | ||
|
|
||
| if (validFqName(descriptorFqn) && (extensionReceiverFqn?.let { validFqName(it) } != false)) { | ||
| SymbolEntity.new { | ||
| fqName = descriptorFqn.toString() | ||
| shortName = descriptorFqn.shortName().toString() | ||
| kind = declaration.accept(ExtractSymbolKind, Unit).rawValue | ||
| visibility = declaration.accept(ExtractSymbolVisibility, Unit).rawValue | ||
| extensionReceiverType = extensionReceiverFqn?.toString() | ||
| } | ||
| } else { | ||
| LOG.warn("Excluding symbol {} from index since its name is too long", descriptorFqn.toString()) | ||
| } | ||
| } | ||
|
|
||
| private fun getFqNames(declaration: DeclarationDescriptor): Pair<FqName, FqName?> { | ||
| val descriptorFqn = declaration.fqNameSafe | ||
| val extensionReceiverFqn = declaration.accept(ExtractSymbolExtensionReceiverType, Unit)?.takeIf { !it.isRoot } | ||
|
|
||
| return Pair(descriptorFqn, extensionReceiverFqn) | ||
| } | ||
|
|
||
| private fun validFqName(fqName: FqName) = | ||
| fqName.toString().length <= MAX_FQNAME_LENGTH | ||
| && fqName.shortName().toString().length <= MAX_SHORT_NAME_LENGTH | ||
|
|
||
| fun query(prefix: String, receiverType: FqName? = null, limit: Int = 20): List<Symbol> = transaction(db) { | ||
| // TODO: Extension completion currently only works if the receiver matches exactly, | ||
| // ideally this should work with subtypes as well | ||
| (Symbols innerJoin FqNames) | ||
| .select { FqNames.shortName.like("$prefix%") and (Symbols.extensionReceiverType eq receiverType?.toString()) } | ||
| .limit(limit) | ||
| SymbolEntity.find { | ||
| (Symbols.shortName like "$prefix%") and (Symbols.extensionReceiverType eq receiverType?.toString()) | ||
| }.limit(limit) | ||
| .map { Symbol( | ||
| fqName = FqName(it[Symbols.fqName]), | ||
| kind = Symbol.Kind.fromRaw(it[Symbols.kind]), | ||
| visibility = Symbol.Visibility.fromRaw(it[Symbols.visibility]), | ||
| extensionReceiverType = it[Symbols.extensionReceiverType]?.let(::FqName) | ||
| fqName = FqName(it.fqName), | ||
| kind = Symbol.Kind.fromRaw(it.kind), | ||
| visibility = Symbol.Visibility.fromRaw(it.visibility), | ||
| extensionReceiverType = it.extensionReceiverType?.let(::FqName) | ||
| ) } | ||
| } | ||
|
|
||
| private fun allDescriptors(module: ModuleDescriptor): Sequence<DeclarationDescriptor> = allPackages(module) | ||
| private fun allDescriptors(module: ModuleDescriptor, exclusions: Sequence<DeclarationDescriptor>): Sequence<DeclarationDescriptor> = allPackages(module) | ||
| .map(module::getPackage) | ||
| .flatMap { | ||
| try { | ||
| it.memberScope.getContributedDescriptors(DescriptorKindFilter.ALL, MemberScope.ALL_NAME_FILTER) | ||
| it.memberScope.getContributedDescriptors( | ||
| DescriptorKindFilter.ALL | ||
| ) { name -> !exclusions.any { declaration -> declaration.name == name } } | ||
| } catch (e: IllegalStateException) { | ||
| LOG.warn("Could not query descriptors in package $it") | ||
| emptyList() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious, could we make
SourceFileadata classto get this implementation for free? Or isn't that possible because it is aninner class?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah,
dataclasses cannot beinnerat the same time.