Skip to content

Commit

Permalink
FIR IDE: introduce out of block modification tracker tests
Browse files Browse the repository at this point in the history
  • Loading branch information
darthorimar committed Nov 23, 2020
1 parent da7b12f commit 911662b
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import org.jetbrains.kotlin.idea.fir.low.level.api.file.builder.ModuleFileCache
import org.jetbrains.kotlin.idea.fir.low.level.api.lazy.resolve.FirLazyDeclarationResolver
import org.jetbrains.kotlin.idea.fir.low.level.api.providers.firIdeProvider
import org.jetbrains.kotlin.idea.fir.low.level.api.util.findSourceNonLocalFirDeclaration
import org.jetbrains.kotlin.idea.search.getKotlinFqName
import org.jetbrains.kotlin.idea.util.getElementTextInContext
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
Expand Down Expand Up @@ -57,7 +59,7 @@ internal class FileStructure(
ktFile.forEachDescendantOfType<KtDeclaration>(
canGoInside = { psi -> psi !is KtFunction && psi !is KtValVarKeywordOwner }
) { declaration ->
if (declaration.isStructureElementContainer()) {
if (FileStructureUtil.isStructureElementContainer(declaration)) {
add(declaration)
}
}
Expand Down Expand Up @@ -104,12 +106,3 @@ internal class FileStructure(
else -> error("Invalid container $container")
}
}

private fun KtDeclaration.isStructureElementContainer(): Boolean {
if (this !is KtClassOrObject && this !is KtDeclarationWithBody && this !is KtProperty && this !is KtTypeAlias) return false
if (this is KtEnumEntry) return false
if (containingClassOrObject is KtEnumEntry) return false
return !KtPsiUtil.isLocal(this)
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/

package org.jetbrains.kotlin.idea.fir.low.level.api.file.structure

import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject

internal object FileStructureUtil {
fun isStructureElementContainer(ktDeclaration: KtDeclaration): Boolean = when {
ktDeclaration !is KtClassOrObject && ktDeclaration !is KtDeclarationWithBody && ktDeclaration !is KtProperty && ktDeclaration !is KtTypeAlias -> false
ktDeclaration is KtEnumEntry -> false
ktDeclaration.containingClassOrObject is KtEnumEntry -> false
else -> !KtPsiUtil.isLocal(ktDeclaration)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ private class FirSessionWithModificationTracker(
val firSession: FirIdeSourcesSession,
) {
private val modificationTracker = firSession.project.service<KotlinFirOutOfBlockModificationTrackerFactory>()
.createModuleOutOfBlockModificationTracker(firSession.moduleInfo.module)
.createModuleWithoutDependenciesOutOfBlockModificationTracker(firSession.moduleInfo.module)

private val timeStamp = modificationTracker.modificationCount

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,42 @@ import com.intellij.pom.tree.TreeAspect
import com.intellij.pom.tree.events.TreeChangeEvent
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.fir.low.level.api.element.builder.getNonLocalContainingInBodyDeclarationWith
import org.jetbrains.kotlin.idea.fir.low.level.api.element.builder.getNonLocalContainingOrThisDeclaration
import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.FileElementFactory
import org.jetbrains.kotlin.idea.util.module
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import java.util.*

internal class KotlinFirModificationTrackerService(project: Project) : Disposable {
init {
val model = PomManager.getModel(project)
model.addModelListener(Listener())
PomManager.getModel(project).addModelListener(Listener())

val connection = project.messageBus.connect(this)
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) {
projectGlobalOutOfBlockInKotlinFilesModificationCount++

// todo increase modificationCountForModule
project.messageBus.connect(this).subscribe(
ProjectTopics.PROJECT_ROOTS,
object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) = increaseModificationCountForAllModules()
}
})
)
}

internal var projectGlobalOutOfBlockInKotlinFilesModificationCount = 0L
var projectGlobalOutOfBlockInKotlinFilesModificationCount = 0L
private set

internal fun getOutOfBlockModificationCountForModules(module: Module): Long =
modificationCountForModule[module] ?: 0L
private val moduleModificationsState = ModuleModificationsState()

fun getOutOfBlockModificationCountForModules(module: Module): Long =
moduleModificationsState.getModificationsCountForModule(module)

private val modificationCountForModule = WeakHashMap<Module, Long>()
private val treeAspect = TreeAspect.getInstance(project)

override fun dispose() {}

private fun increaseModificationCountForAllModules() {
projectGlobalOutOfBlockInKotlinFilesModificationCount++
moduleModificationsState.increaseModificationCountForAllModules()
}

private inner class Listener : PomModelListener {
override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean =
treeAspect == aspect

override fun modelChanged(event: PomModelEvent) {
val changeSet = event.getChangeSet(treeAspect) as TreeChangeEvent? ?: return
if (changeSet.rootElement.psi.language != KotlinLanguage.INSTANCE) return
Expand All @@ -66,7 +68,7 @@ internal class KotlinFirModificationTrackerService(project: Project) : Disposabl
isOutOfBlockChangeInAnyModule = isOutOfBlockChangeInAnyModule || isOutOfBlock
if (isOutOfBlock) {
element.psi.module?.let { module ->
modificationCountForModule.compute(module) { _, value -> (value ?: 0) + 1 }
moduleModificationsState.increaseModificationCountForModule(module)
}
}
}
Expand All @@ -84,8 +86,33 @@ internal class KotlinFirModificationTrackerService(project: Project) : Disposabl
!FileElementFactory.isReanalyzableContainer(container)
}
}
}
}

override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean =
treeAspect == aspect
private class ModuleModificationsState {
private val modificationCountForModule = hashMapOf<Module, ModuleModifications>()
private var state: Long = 0L

fun getModificationsCountForModule(module: Module) = modificationCountForModule.compute(module) { _, modifications ->
when {
modifications == null -> ModuleModifications(0, state)
modifications.state == state -> modifications
else -> ModuleModifications(modificationsCount = modifications.modificationsCount + 1, state = state)
}
}!!.modificationsCount

fun increaseModificationCountForAllModules() {
state++
}

fun increaseModificationCountForModule(module: Module) {
modificationCountForModule.compute(module) { _, modifications ->
when (modifications) {
null -> ModuleModifications(0, state)
else -> ModuleModifications(ModuleModifications(0, state).modificationsCount + 1, state)
}
}
}

private data class ModuleModifications(val modificationsCount: Long, val state: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ class KotlinFirOutOfBlockModificationTrackerFactory(private val project: Project
fun createProjectWideOutOfBlockModificationTracker(): ModificationTracker =
KotlinFirOutOfBlockModificationTracker(project)

fun createModuleOutOfBlockModificationTracker(module: Module): ModificationTracker =
fun createModuleWithoutDependenciesOutOfBlockModificationTracker(module: Module): ModificationTracker =
KotlinFirOutOfBlockModuleModificationTracker(module)

}

fun Project.createProjectWideOutOfBlockModificationTracker() =
service<KotlinFirOutOfBlockModificationTrackerFactory>().createProjectWideOutOfBlockModificationTracker()

fun Module.createModuleWithoutDependenciesOutOfBlockModificationTracker() =
project.service<KotlinFirOutOfBlockModificationTrackerFactory>().createModuleWithoutDependenciesOutOfBlockModificationTracker(this)

private class KotlinFirOutOfBlockModificationTracker(project: Project) : ModificationTracker {
private val trackerService = project.service<KotlinFirModificationTrackerService>()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/

package org.jetbrains.kotlin.idea.fir.low.level.api.trackers

import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiManager
import com.intellij.testFramework.PsiTestUtil
import junit.framework.Assert
import org.jetbrains.kotlin.idea.stubs.AbstractMultiModuleTest
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.idea.util.rootManager
import org.jetbrains.kotlin.idea.util.sourceRoots
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import java.nio.file.Files
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.writeText

class KotlinModuleOutOfBlockTrackerTest : AbstractMultiModuleTest() {
override fun getTestDataPath(): String = error("Should not be called")

fun testThatModuleOutOfBlockChangeInfluenceOnlySingleModule() {
val moduleA = createModuleWithModificationTracker("a") {
listOf(
FileWithText("main.kt", "fun main() = 10")
)
}
val moduleB = createModuleWithModificationTracker("b")
val moduleC = createModuleWithModificationTracker("c")


val moduleAWithTracker = ModuleWithModificationTracker(moduleA)
val moduleBWithTracker = ModuleWithModificationTracker(moduleB)
val moduleCWithTracker = ModuleWithModificationTracker(moduleC)

moduleA.typeInFunctionBody("main.kt", textAfterTyping = "fun main() = hello10")

Assert.assertTrue(
"Out of block modification count for module A with out of block should change after typing, modification count is ${moduleAWithTracker.modificationCount}",
moduleAWithTracker.changed()
)
Assert.assertFalse(
"Out of block modification count for module B without out of block should not change after typing, modification count is ${moduleBWithTracker.modificationCount}",
moduleBWithTracker.changed()
)
Assert.assertFalse(
"Out of block modification count for module C without out of block should not change after typing, modification count is ${moduleCWithTracker.modificationCount}",
moduleCWithTracker.changed()
)
}

fun testThatInEveryModuleOutOfBlockWillHappenAfterContentRootChange() {
val moduleA = createModuleWithModificationTracker("a")
val moduleB = createModuleWithModificationTracker("b")
val moduleC = createModuleWithModificationTracker("c")

val moduleAWithTracker = ModuleWithModificationTracker(moduleA)
val moduleBWithTracker = ModuleWithModificationTracker(moduleB)
val moduleCWithTracker = ModuleWithModificationTracker(moduleC)

runWriteAction {
moduleA.sourceRoots.first().createChildData(/* requestor = */ null, "file.kt")
}

Assert.assertTrue(
"Out of block modification count for module A should change after content root change, modification count is ${moduleAWithTracker.modificationCount}",
moduleAWithTracker.changed()
)
Assert.assertTrue(
"Out of block modification count for module B should change after content root change, modification count is ${moduleBWithTracker.modificationCount}",
moduleBWithTracker.changed()
)
Assert.assertTrue(
"Out of block modification count for module C should change after content root change modification count is ${moduleCWithTracker.modificationCount}",
moduleCWithTracker.changed()
)
}

private fun Module.typeInFunctionBody(fileName: String, textAfterTyping: String) {
val file = "${sourceRoots.first().url}/$fileName"
val virtualFile = VirtualFileManager.getInstance().findFileByUrl(file)!!
val ktFile = PsiManager.getInstance(project).findFile(virtualFile) as KtFile
configureByExistingFile(virtualFile)

val singleFunction = ktFile.declarations.single() as KtNamedFunction

editor.caretModel.moveToOffset(singleFunction.bodyExpression!!.textOffset)
type("hello")
PsiDocumentManager.getInstance(project).commitAllDocuments()
Assert.assertEquals(textAfterTyping, ktFile.text)
}

@OptIn(ExperimentalPathApi::class)
private fun createModuleWithModificationTracker(
name: String,
createFiles: () -> List<FileWithText> = { emptyList() },
): Module {
val tmpDir = createTempDirectory().toPath()
createFiles().forEach { file ->
Files.createFile(tmpDir.resolve(file.name)).writeText(file.text)
}
val module: Module = createModule("$tmpDir/$name", moduleType)
val root = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(tmpDir.toFile())!!
WriteCommandAction.writeCommandAction(module.project).run<RuntimeException> {
root.refresh(false, true)
}

PsiTestUtil.addSourceContentToRoots(module, root)
return module
}

private data class FileWithText(val name: String, val text: String)

private class ModuleWithModificationTracker(module: Module) {
private val modificationTracker = module.createModuleWithoutDependenciesOutOfBlockModificationTracker()
private val initialModificationCount = modificationTracker.modificationCount

val modificationCount: Long
get() = modificationTracker.modificationCount

fun changed(): Boolean =
modificationTracker.modificationCount != initialModificationCount
}
}

0 comments on commit 911662b

Please sign in to comment.