Files
kotlin-fork/idea/tests/org/jetbrains/kotlin/findUsages/AbstractFindUsagesTest.kt
T
2020-11-23 15:31:14 +01:00

430 lines
17 KiB
Kotlin

/*
* Copyright 2010-2019 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.findUsages
import com.intellij.codeInsight.TargetElementUtil
import com.intellij.find.FindManager
import com.intellij.find.findUsages.FindUsagesHandler
import com.intellij.find.findUsages.FindUsagesOptions
import com.intellij.find.findUsages.JavaFindUsagesHandler
import com.intellij.find.findUsages.JavaFindUsagesHandlerFactory
import com.intellij.find.impl.FindManagerImpl
import com.intellij.lang.properties.psi.PropertiesFile
import com.intellij.lang.properties.psi.Property
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMember
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.UsefulTestCase
import com.intellij.usageView.UsageInfo
import com.intellij.usages.TextChunk
import com.intellij.usages.UsageInfo2UsageAdapter
import com.intellij.usages.UsageViewPresentation
import com.intellij.usages.impl.rules.UsageType
import com.intellij.usages.impl.rules.UsageTypeProvider
import com.intellij.usages.rules.UsageFilteringRule
import com.intellij.usages.rules.UsageGroupingRule
import com.intellij.util.CommonProcessors
import org.jetbrains.kotlin.executeOnPooledThreadInReadAction
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.util.clearDialogsResults
import org.jetbrains.kotlin.idea.core.util.setDialogsResult
import org.jetbrains.kotlin.idea.findUsages.handlers.KotlinFindMemberUsagesHandler
import org.jetbrains.kotlin.idea.refactoring.CHECK_SUPER_METHODS_YES_NO_DIALOG
import org.jetbrains.kotlin.idea.search.usagesSearch.ExpressionsOfTypeProcessor
import org.jetbrains.kotlin.idea.test.*
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.runReadActionInSmartMode
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.jetbrains.kotlin.test.KotlinTestUtils
import java.io.File
import java.util.*
import kotlin.collections.LinkedHashSet
abstract class AbstractFindUsagesWithDisableComponentSearchTest : AbstractFindUsagesTest() {
override fun <T : PsiElement> doTest(path: String) {
val oldValue = KotlinFindMemberUsagesHandler.forceDisableComponentAndDestructionSearch
try {
KotlinFindMemberUsagesHandler.forceDisableComponentAndDestructionSearch = true
super.doTest<T>(path)
} finally {
KotlinFindMemberUsagesHandler.forceDisableComponentAndDestructionSearch = oldValue
}
}
override val prefixForResults = "DisabledComponents."
}
abstract class AbstractFindUsagesTest : KotlinLightCodeInsightFixtureTestCase() {
override fun getProjectDescriptor(): LightProjectDescriptor = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
// used in Spring tests (outside main project!)
protected open fun extraConfig(path: String) {
}
protected open val prefixForResults = ""
protected open fun <T : PsiElement> doTest(path: String) {
val mainFile = File(path)
val mainFileName = mainFile.name
val mainFileText = FileUtil.loadFile(mainFile, true)
val prefix = mainFileName.substringBefore(".") + "."
val isPropertiesFile = FileUtilRt.getExtension(path) == "properties"
InTextDirectivesUtils.findStringWithPrefixes(mainFileText, "// IGNORE: ")?.let {
println("test $mainFileName is ignored")
return
}
// if (isFirPlugin) {
// InTextDirectivesUtils.findStringWithPrefixes(mainFileText, "// FIR_IGNORE")?.let {
// return
// }
// }
val isFindFileUsages = InTextDirectivesUtils.isDirectiveDefined(mainFileText, "## FIND_FILE_USAGES")
@Suppress("UNCHECKED_CAST")
val caretElementClass = (if (!isPropertiesFile) {
val caretElementClassNames = InTextDirectivesUtils.findLinesWithPrefixesRemoved(mainFileText, "// PSI_ELEMENT: ")
Class.forName(caretElementClassNames.single())
} else if (isFindFileUsages) {
PropertiesFile::class.java
} else {
Property::class.java
}) as Class<T>
val fixtureClasses = InTextDirectivesUtils.findListWithPrefixes(mainFileText, "// FIXTURE_CLASS: ")
for (fixtureClass in fixtureClasses) {
TestFixtureExtension.loadFixture(fixtureClass, myFixture.module)
}
try {
extraConfig(path)
val parser = OptionsParser.getParserByPsiElementClass(caretElementClass)
val rootPath = path.substringBeforeLast("/") + "/"
val rootDir = File(rootPath)
val extraFiles = rootDir.listFiles { _, name ->
if (!name.startsWith(prefix) || name == mainFileName) return@listFiles false
val ext = FileUtilRt.getExtension(name)
ext in SUPPORTED_EXTENSIONS && !name.endsWith(".results.txt")
}.orEmpty()
for (file in extraFiles) {
myFixture.configureByFile(file.name)
}
myFixture.configureByFile(mainFileName)
if ((myFixture.file as? KtFile)?.isScript() == true) {
ScriptConfigurationManager.updateScriptDependenciesSynchronously(myFixture.file)
}
if (!isFirPlugin) {
(myFixture.file as? KtFile)?.let { ktFile ->
val diagnosticsProvider: (KtFile) -> Diagnostics = { it.analyzeWithAllCompilerChecks().bindingContext.diagnostics }
DirectiveBasedActionUtils.checkForUnexpectedWarnings(ktFile, diagnosticsProvider)
DirectiveBasedActionUtils.checkForUnexpectedErrors(ktFile, diagnosticsProvider)
}
}
val caretElement = when {
InTextDirectivesUtils.isDirectiveDefined(mainFileText, "// FIND_BY_REF") -> {
executeOnPooledThreadInReadAction {
TargetElementUtil.findTargetElement(
myFixture.editor,
TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED or TargetElementUtil.getInstance().referenceSearchFlags
)
}!!
}
isFindFileUsages -> myFixture.file
else -> myFixture.elementAtCaret
}
UsefulTestCase.assertInstanceOf(caretElement, caretElementClass)
val containingFile = caretElement.containingFile
val isLibraryElement = containingFile != null && ProjectRootsUtil.isLibraryFile(project, containingFile.virtualFile)
val options = parser?.parse(mainFileText, project)
// Ensure that search by sources (if present) and decompiled declarations gives the same results
val prefixForCheck = prefix + prefixForResults
if (isLibraryElement) {
val originalElement = caretElement.originalElement
findUsagesAndCheckResults(
mainFileText,
prefixForCheck,
rootPath,
originalElement,
options,
project,
alwaysAppendFileName = false,
isFirPlugin = isFirPlugin
)
val navigationElement = caretElement.navigationElement
if (navigationElement !== originalElement) {
findUsagesAndCheckResults(
mainFileText,
prefixForCheck,
rootPath,
navigationElement,
options,
project,
alwaysAppendFileName = false,
isFirPlugin = isFirPlugin
)
}
} else {
findUsagesAndCheckResults(
mainFileText,
prefixForCheck,
rootPath,
caretElement,
options,
project,
alwaysAppendFileName = false,
isFirPlugin = isFirPlugin
)
}
} finally {
fixtureClasses.forEach { TestFixtureExtension.unloadFixture(it) }
}
}
companion object {
val SUPPORTED_EXTENSIONS = setOf("kt", "kts", "java", "xml", "properties", "txt", "groovy")
val USAGE_VIEW_PRESENTATION = UsageViewPresentation()
internal fun getUsageAdapters(
filters: Collection<UsageFilteringRule>,
usageInfos: Collection<UsageInfo>
): Collection<UsageInfo2UsageAdapter> {
return usageInfos
.map(::UsageInfo2UsageAdapter)
.filter { usageAdapter -> filters.all { it.isVisible(usageAdapter) } }
}
internal fun getUsageType(element: PsiElement?): UsageType? {
if (element == null) return null
if (element.getNonStrictParentOfType<PsiComment>() != null) {
return UsageType.COMMENT_USAGE
}
@Suppress("DEPRECATION")
val providers = Extensions.getExtensions(UsageTypeProvider.EP_NAME)
return providers
.mapNotNull {
it.getUsageType(element)
}
.firstOrNull()
?: UsageType.UNCLASSIFIED
}
internal fun <T> instantiateClasses(mainFileText: String, directive: String): Collection<T> {
val filteringRuleClassNames = InTextDirectivesUtils.findLinesWithPrefixesRemoved(mainFileText, directive)
return filteringRuleClassNames
.map {
@Suppress("UNCHECKED_CAST")
(Class.forName(it).newInstance() as T)
}
}
}
}
internal fun <T : PsiElement> findUsagesAndCheckResults(
mainFileText: String,
prefix: String,
rootPath: String,
caretElement: T,
options: FindUsagesOptions?,
project: Project,
alwaysAppendFileName: Boolean = false,
isFirPlugin: Boolean = false
) {
val highlightingMode = InTextDirectivesUtils.isDirectiveDefined(mainFileText, "// HIGHLIGHTING")
var log: String? = null
val logList = LinkedHashSet<String>()
val usageInfos = try {
if (ExpressionsOfTypeProcessor.mode !== ExpressionsOfTypeProcessor.Mode.ALWAYS_PLAIN) {
ExpressionsOfTypeProcessor.testLog = logList
}
if (InTextDirectivesUtils.isDirectiveDefined(mainFileText, "// PLAIN_WHEN_NEEDED")) {
ExpressionsOfTypeProcessor.mode = ExpressionsOfTypeProcessor.Mode.PLAIN_WHEN_NEEDED
}
val searchSuperDeclaration =
InTextDirectivesUtils.findLinesWithPrefixesRemoved(mainFileText, "$CHECK_SUPER_METHODS_YES_NO_DIALOG:").firstOrNull() != "no"
findUsages(caretElement, options, highlightingMode, project, searchSuperDeclaration, isFirPlugin)
} finally {
ExpressionsOfTypeProcessor.testLog = null
if (logList.size > 0) {
log = logList.sorted().joinToString("\n")
}
ExpressionsOfTypeProcessor.mode = ExpressionsOfTypeProcessor.Mode.ALWAYS_SMART
}
val filteringRules = AbstractFindUsagesTest.instantiateClasses<UsageFilteringRule>(mainFileText, "// FILTERING_RULES: ")
val groupingRules = AbstractFindUsagesTest.instantiateClasses<UsageGroupingRule>(mainFileText, "// GROUPING_RULES: ")
val filteredUsages = AbstractFindUsagesTest.getUsageAdapters(filteringRules, usageInfos)
val usageFiles = filteredUsages.map { it.file.name }.distinct()
val appendFileName = alwaysAppendFileName || usageFiles.size > 1
val convertToString: (UsageInfo2UsageAdapter) -> String = { usageAdapter ->
var groupAsString = groupingRules.joinToString(", ") { it.groupUsage(usageAdapter)?.getText(null) ?: "" }
if (groupAsString.isNotEmpty()) {
groupAsString = "($groupAsString) "
}
val usageType = executeOnPooledThreadInReadAction {
AbstractFindUsagesTest.getUsageType(usageAdapter.element)
}
val usageTypeAsString = usageType?.toString(AbstractFindUsagesTest.USAGE_VIEW_PRESENTATION) ?: "null"
val usageChunks = ArrayList<TextChunk>()
usageChunks.addAll(usageAdapter.presentation.text.asList())
usageChunks.add(1, TextChunk(TextAttributes(), " ")) // add space after line number
buildString {
if (appendFileName) {
append("[").append(usageAdapter.file.name).append("] ")
}
append(usageTypeAsString)
append(" ")
append(groupAsString)
append(usageChunks.joinToString(""))
}
}
val finalUsages = filteredUsages.map(convertToString).sorted()
KotlinTestUtils.assertEqualsToFile(File(rootPath, prefix + "results.txt"), finalUsages.joinToString("\n"))
if (log != null) {
KotlinTestUtils.assertEqualsToFile(File(rootPath, prefix + "log"), log)
// if log is empty then compare results with plain search
try {
ExpressionsOfTypeProcessor.mode = ExpressionsOfTypeProcessor.Mode.ALWAYS_PLAIN
findUsagesAndCheckResults(
mainFileText,
prefix,
rootPath,
caretElement,
options,
project,
alwaysAppendFileName = false,
isFirPlugin = isFirPlugin
)
} finally {
ExpressionsOfTypeProcessor.mode = ExpressionsOfTypeProcessor.Mode.ALWAYS_SMART
}
}
}
internal fun findUsages(
targetElement: PsiElement,
options: FindUsagesOptions?,
highlightingMode: Boolean,
project: Project,
searchSuperDeclaration: Boolean = true,
isFirPlugin: Boolean = false
): Collection<UsageInfo> {
try {
val handler: FindUsagesHandler = if (targetElement is PsiMember)
JavaFindUsagesHandler(targetElement, JavaFindUsagesHandlerFactory(project))
else {
if (!searchSuperDeclaration) {
setDialogsResult(CHECK_SUPER_METHODS_YES_NO_DIALOG, Messages.NO)
}
val findManagerImpl = FindManager.getInstance(project) as FindManagerImpl
findManagerImpl.findUsagesManager.getFindUsagesHandler(targetElement, false)
?: error("Cannot find handler for: $targetElement")
}
@Suppress("NAME_SHADOWING")
val options = options ?: handler.getFindUsagesOptions(null)
options.searchScope = GlobalSearchScope.allScope(project)
val processor = CommonProcessors.CollectProcessor<UsageInfo>()
for (psiElement in handler.primaryElements + handler.secondaryElements) {
if (highlightingMode) {
if (isFirPlugin) {
ProgressManager.getInstance().run(
object : Task.Modal(project, "", false) {
override fun run(indicator: ProgressIndicator) {
project.runReadActionInSmartMode {
for (reference in handler.findReferencesToHighlight(psiElement, options.searchScope)) {
processor.process(UsageInfo(reference))
}
}
}
},
)
} else {
project.runReadActionInSmartMode {
for (reference in handler.findReferencesToHighlight(psiElement, options.searchScope)) {
processor.process(UsageInfo(reference))
}
}
}
} else {
ProgressManager.getInstance().run(
object : Task.Modal(project, "", false) {
override fun run(indicator: ProgressIndicator) {
project.runReadActionInSmartMode {
handler.processElementUsages(psiElement, processor, options)
}
}
},
)
}
}
return processor.results
} finally {
clearDialogsResults()
}
}