[webSymbols] Make WebSymbols part of JavaScript test framework to allow 3rd parties to run tests in Gradle setup

GitOrigin-RevId: 7bbdaa39e8b05c2537f21c68f36b8f72887837d8
This commit is contained in:
Piotr Tomiak
2025-01-27 11:54:08 +01:00
committed by intellij-monorepo-bot
parent 980706b939
commit b853b4f3af
15 changed files with 1001 additions and 865 deletions

2
.idea/modules.xml generated
View File

@@ -900,6 +900,8 @@
<module fileurl="file://$PROJECT_DIR$/platform/warmup/intellij.platform.warmup.iml" filepath="$PROJECT_DIR$/platform/warmup/intellij.platform.warmup.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/warmup/performanceTesting/intellij.platform.warmup.performanceTesting.iml" filepath="$PROJECT_DIR$/platform/warmup/performanceTesting/intellij.platform.warmup.performanceTesting.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.iml" filepath="$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.testFramework.iml" filepath="$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.testFramework.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.tests.iml" filepath="$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/whatsNew/intellij.platform.whatsNew.iml" filepath="$PROJECT_DIR$/platform/whatsNew/intellij.platform.whatsNew.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/workspace/jps/intellij.platform.workspace.jps.iml" filepath="$PROJECT_DIR$/platform/workspace/jps/intellij.platform.workspace.jps.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/workspace/jps/tests/intellij.platform.workspace.jps.tests.iml" filepath="$PROJECT_DIR$/platform/workspace/jps/tests/intellij.platform.workspace.jps.tests.iml" />

View File

@@ -6,7 +6,6 @@
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
@@ -28,7 +27,5 @@
<orderEntry type="library" name="jetbrains.markdown" level="project" />
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="module" module-name="intellij.platform.markdown.utils" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.java.rt" scope="TEST" />
</component>
</module>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/testFramework">
<sourceFolder url="file://$MODULE_DIR$/testFramework" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.platform.webSymbols" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.editor.ex" />
<orderEntry type="library" name="JUnit4" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.usageView" />
<orderEntry type="module" module-name="intellij.platform.refactoring" />
<orderEntry type="library" name="jackson-databind" level="project" />
<orderEntry type="module" module-name="intellij.platform.analysis.impl" />
<orderEntry type="library" name="jackson" level="project" />
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
<orderEntry type="library" name="fastutil-min" level="project" />
<orderEntry type="library" name="commons-compress" level="project" />
<orderEntry type="library" name="jetbrains.markdown" level="project" />
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="module" module-name="intellij.platform.markdown.utils" />
<orderEntry type="module" module-name="intellij.platform.testFramework" />
<orderEntry type="module" module-name="intellij.java.rt" />
</component>
</module>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/testData">
<sourceFolder url="file://$MODULE_DIR$/testData" type="java-test-resource" />
</content>
<content url="file://$MODULE_DIR$/testSrc">
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.editor.ex" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.usageView" />
<orderEntry type="module" module-name="intellij.platform.refactoring" />
<orderEntry type="library" name="jackson-databind" level="project" />
<orderEntry type="module" module-name="intellij.platform.analysis.impl" />
<orderEntry type="library" name="jackson" level="project" />
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
<orderEntry type="library" name="fastutil-min" level="project" />
<orderEntry type="library" name="commons-compress" level="project" />
<orderEntry type="library" name="jetbrains.markdown" level="project" />
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="module" module-name="intellij.platform.markdown.utils" />
<orderEntry type="module" module-name="intellij.platform.testFramework" />
<orderEntry type="module" module-name="intellij.platform.webSymbols" />
<orderEntry type="module" module-name="intellij.platform.webSymbols.testFramework" />
<orderEntry type="module" module-name="intellij.java.rt" />
</component>
</module>

View File

@@ -17,6 +17,7 @@ import com.intellij.webSymbols.query.WebSymbolsListSymbolsQueryParams
import com.intellij.webSymbols.query.WebSymbolsNameMatchQueryParams
import com.intellij.webSymbols.query.WebSymbolsQueryParams
import com.intellij.webSymbols.webTypes.json.WebTypes
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon
@@ -36,9 +37,11 @@ internal fun Icon.scaleToHeight(height: Int): Icon {
return IconUtil.scale(this, null, scale)
}
internal fun <T> List<T>.selectBest(segmentsProvider: (T) -> List<WebSymbolNameSegment>,
priorityProvider: (T) -> WebSymbol.Priority?,
isExtension: (T) -> Boolean) =
internal fun <T> List<T>.selectBest(
segmentsProvider: (T) -> List<WebSymbolNameSegment>,
priorityProvider: (T) -> WebSymbol.Priority?,
isExtension: (T) -> Boolean,
) =
if (size > 1) {
var bestWeight: IntArray = intArrayOf(0, 0, 0)
@@ -109,5 +112,6 @@ internal fun WebSymbolNameSegment.copy(
): WebSymbolNameSegmentImpl =
(this as WebSymbolNameSegmentImpl).copy(apiStatus, priority, proximity, problem, symbols)
internal fun WebSymbolNameSegment.canUnwrapSymbols(): Boolean =
@ApiStatus.Internal
fun WebSymbolNameSegment.canUnwrapSymbols(): Boolean =
(this as WebSymbolNameSegmentImpl).canUnwrapSymbols()

View File

@@ -12,9 +12,11 @@ import com.intellij.webSymbols.query.WebSymbolNameConversionRules
import com.intellij.webSymbols.query.WebSymbolNameConverter
import com.intellij.webSymbols.query.WebSymbolNamesProvider
import com.intellij.webSymbols.query.WebSymbolNamesProvider.Target.*
import org.jetbrains.annotations.ApiStatus
import java.util.*
internal class WebSymbolNamesProviderImpl(
@ApiStatus.Internal
class WebSymbolNamesProviderImpl(
private val framework: FrameworkId?,
private val configuration: List<WebSymbolNameConversionRules>,
private val modificationTracker: ModificationTracker,
@@ -76,9 +78,11 @@ internal class WebSymbolNamesProviderImpl(
else
listOf(qualifiedName.name)
override fun adjustRename(qualifiedName: WebSymbolQualifiedName,
newName: String,
occurence: String): String {
override fun adjustRename(
qualifiedName: WebSymbolQualifiedName,
newName: String,
occurence: String,
): String {
if (qualifiedName.name == occurence) return newName
val oldVariants = getNames(qualifiedName, WebSymbolNamesProvider.Target.NAMES_QUERY)

View File

@@ -9,22 +9,29 @@ import com.intellij.util.applyIf
import com.intellij.util.asSafely
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.util.containers.Stack
import com.intellij.webSymbols.*
import com.intellij.webSymbols.WebSymbol
import com.intellij.webSymbols.WebSymbolQualifiedKind
import com.intellij.webSymbols.WebSymbolQualifiedName
import com.intellij.webSymbols.WebSymbolsScope
import com.intellij.webSymbols.completion.WebSymbolCodeCompletionItem
import com.intellij.webSymbols.context.WebSymbolsContext
import com.intellij.webSymbols.impl.filterByQueryParams
import com.intellij.webSymbols.impl.selectBest
import com.intellij.webSymbols.query.*
import com.intellij.webSymbols.utils.*
import org.jetbrains.annotations.ApiStatus
import java.util.*
import kotlin.math.max
import kotlin.math.min
internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
override val namesProvider: WebSymbolNamesProvider,
override val resultsCustomizer: WebSymbolsQueryResultsCustomizer,
override val context: WebSymbolsContext,
override val allowResolve: Boolean) : WebSymbolsQueryExecutor {
@ApiStatus.Internal
class WebSymbolsQueryExecutorImpl(
rootScope: List<WebSymbolsScope>,
override val namesProvider: WebSymbolNamesProvider,
override val resultsCustomizer: WebSymbolsQueryResultsCustomizer,
override val context: WebSymbolsContext,
override val allowResolve: Boolean,
) : WebSymbolsQueryExecutor {
private val rootScope: List<WebSymbolsScope> = initializeCompoundScopes(rootScope)
@@ -60,28 +67,34 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
}
}
override fun runNameMatchQuery(path: List<WebSymbolQualifiedName>,
virtualSymbols: Boolean,
abstractSymbols: Boolean,
strictScope: Boolean,
additionalScope: List<WebSymbolsScope>): List<WebSymbol> =
override fun runNameMatchQuery(
path: List<WebSymbolQualifiedName>,
virtualSymbols: Boolean,
abstractSymbols: Boolean,
strictScope: Boolean,
additionalScope: List<WebSymbolsScope>,
): List<WebSymbol> =
runNameMatchQuery(path, WebSymbolsNameMatchQueryParams.create(this, virtualSymbols, abstractSymbols, strictScope), additionalScope)
override fun runListSymbolsQuery(path: List<WebSymbolQualifiedName>,
qualifiedKind: WebSymbolQualifiedKind,
expandPatterns: Boolean,
virtualSymbols: Boolean,
abstractSymbols: Boolean,
strictScope: Boolean,
additionalScope: List<WebSymbolsScope>): List<WebSymbol> =
override fun runListSymbolsQuery(
path: List<WebSymbolQualifiedName>,
qualifiedKind: WebSymbolQualifiedKind,
expandPatterns: Boolean,
virtualSymbols: Boolean,
abstractSymbols: Boolean,
strictScope: Boolean,
additionalScope: List<WebSymbolsScope>,
): List<WebSymbol> =
runListSymbolsQuery(path + qualifiedKind.withName(""),
WebSymbolsListSymbolsQueryParams.create(this, expandPatterns = expandPatterns, virtualSymbols = virtualSymbols,
abstractSymbols = abstractSymbols, strictScope = strictScope), additionalScope)
abstractSymbols = abstractSymbols, strictScope = strictScope), additionalScope)
override fun runCodeCompletionQuery(path: List<WebSymbolQualifiedName>,
position: Int,
virtualSymbols: Boolean,
additionalScope: List<WebSymbolsScope>): List<WebSymbolCodeCompletionItem> =
override fun runCodeCompletionQuery(
path: List<WebSymbolQualifiedName>,
position: Int,
virtualSymbols: Boolean,
additionalScope: List<WebSymbolsScope>,
): List<WebSymbolCodeCompletionItem> =
runCodeCompletionQuery(path, WebSymbolsCodeCompletionQueryParams.create(this, position, virtualSymbols), additionalScope)
override fun withNameConversionRules(rules: List<WebSymbolNameConversionRules>): WebSymbolsQueryExecutor =
@@ -103,11 +116,13 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
return rootScope.flatMap {
if (it is WebSymbolsCompoundScope) {
it.getScopes(compoundScopeQueryExecutor)
} else {
}
else {
listOf(it)
}
}
} else return rootScope
}
else return rootScope
}
private fun buildQueryScope(additionalScope: List<WebSymbolsScope>): MutableSet<WebSymbolsScope> {
@@ -122,12 +137,16 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
return finalScope
}
private fun runNameMatchQuery(path: List<WebSymbolQualifiedName>,
queryParams: WebSymbolsNameMatchQueryParams,
additionalScope: List<WebSymbolsScope>): List<WebSymbol> =
runQuery(path, queryParams, additionalScope) { finalContext: Collection<WebSymbolsScope>,
qualifiedName: WebSymbolQualifiedName,
params: WebSymbolsNameMatchQueryParams ->
private fun runNameMatchQuery(
path: List<WebSymbolQualifiedName>,
queryParams: WebSymbolsNameMatchQueryParams,
additionalScope: List<WebSymbolsScope>,
): List<WebSymbol> =
runQuery(path, queryParams, additionalScope) {
finalContext: Collection<WebSymbolsScope>,
qualifiedName: WebSymbolQualifiedName,
params: WebSymbolsNameMatchQueryParams,
->
val result = finalContext
.takeLastUntilExclusiveScopeFor(qualifiedName.qualifiedKind)
.asSequence()
@@ -143,11 +162,15 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
result
}
private fun runListSymbolsQuery(path: List<WebSymbolQualifiedName>, queryParams: WebSymbolsListSymbolsQueryParams,
additionalScope: List<WebSymbolsScope>): List<WebSymbol> =
runQuery(path, queryParams, additionalScope) { finalContext: Collection<WebSymbolsScope>,
qualifiedName: WebSymbolQualifiedName,
params: WebSymbolsListSymbolsQueryParams ->
private fun runListSymbolsQuery(
path: List<WebSymbolQualifiedName>, queryParams: WebSymbolsListSymbolsQueryParams,
additionalScope: List<WebSymbolsScope>,
): List<WebSymbol> =
runQuery(path, queryParams, additionalScope) {
finalContext: Collection<WebSymbolsScope>,
qualifiedName: WebSymbolQualifiedName,
params: WebSymbolsListSymbolsQueryParams,
->
val result = finalContext
.takeLastUntilExclusiveScopeFor(qualifiedName.qualifiedKind)
.asSequence()
@@ -190,11 +213,15 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
result
}
private fun runCodeCompletionQuery(path: List<WebSymbolQualifiedName>, queryParams: WebSymbolsCodeCompletionQueryParams,
additionalScope: List<WebSymbolsScope>): List<WebSymbolCodeCompletionItem> =
runQuery(path, queryParams, additionalScope) { finalContext: Collection<WebSymbolsScope>,
pathSection: WebSymbolQualifiedName,
params: WebSymbolsCodeCompletionQueryParams ->
private fun runCodeCompletionQuery(
path: List<WebSymbolQualifiedName>, queryParams: WebSymbolsCodeCompletionQueryParams,
additionalScope: List<WebSymbolsScope>,
): List<WebSymbolCodeCompletionItem> =
runQuery(path, queryParams, additionalScope) {
finalContext: Collection<WebSymbolsScope>,
pathSection: WebSymbolQualifiedName,
params: WebSymbolsCodeCompletionQueryParams,
->
var proximityBase = 0
var nextProximityBase = 0
var previousName: String? = null
@@ -243,7 +270,8 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
context: Collection<WebSymbolsScope>,
pathSection: WebSymbolQualifiedName,
params: P,
) -> List<T>): List<T> {
) -> List<T>,
): List<T> {
ProgressManager.checkCanceled()
if (path.isEmpty()) return emptyList()
@@ -306,8 +334,10 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
?: item
}
private fun WebSymbol.expandPattern(context: Stack<WebSymbolsScope>,
params: WebSymbolsListSymbolsQueryParams): List<WebSymbol> =
private fun WebSymbol.expandPattern(
context: Stack<WebSymbolsScope>,
params: WebSymbolsListSymbolsQueryParams,
): List<WebSymbol> =
pattern?.let { pattern ->
context.push(this)
try {

View File

@@ -28,6 +28,7 @@ import com.intellij.webSymbols.patterns.impl.applyIcons
import com.intellij.webSymbols.query.*
import com.intellij.webSymbols.query.impl.WebSymbolMatchImpl
import com.intellij.webSymbols.references.WebSymbolReferenceProblem.ProblemKind
import org.jetbrains.annotations.ApiStatus
import java.util.*
import javax.swing.Icon
import kotlin.contracts.ExperimentalContracts
@@ -137,9 +138,11 @@ fun WebSymbolNameSegment.withSymbols(symbols: List<WebSymbol>): WebSymbolNameSeg
fun WebSymbolMatch.withSegments(segments: List<WebSymbolNameSegment>): WebSymbolMatch =
(this as WebSymbolMatchImpl).withSegments(segments)
fun WebSymbol.match(nameToMatch: String,
params: WebSymbolsNameMatchQueryParams,
context: Stack<WebSymbolsScope>): List<WebSymbol> {
fun WebSymbol.match(
nameToMatch: String,
params: WebSymbolsNameMatchQueryParams,
context: Stack<WebSymbolsScope>,
): List<WebSymbol> {
pattern?.let { pattern ->
context.push(this)
try {
@@ -168,9 +171,11 @@ fun WebSymbol.match(nameToMatch: String,
}
}
fun WebSymbol.toCodeCompletionItems(name: String,
params: WebSymbolsCodeCompletionQueryParams,
context: Stack<WebSymbolsScope>): List<WebSymbolCodeCompletionItem> =
fun WebSymbol.toCodeCompletionItems(
name: String,
params: WebSymbolsCodeCompletionQueryParams,
context: Stack<WebSymbolsScope>,
): List<WebSymbolCodeCompletionItem> =
pattern?.let { pattern ->
context.push(this)
try {
@@ -286,13 +291,15 @@ fun WebSymbolApiStatus?.coalesceWith(other: WebSymbolApiStatus?): WebSymbolApiSt
}
is WebSymbolApiStatus.Experimental -> when (other) {
is WebSymbolApiStatus.Obsolete,
is WebSymbolApiStatus.Deprecated -> other
is WebSymbolApiStatus.Deprecated,
-> other
else -> this
}
is WebSymbolApiStatus.Stable -> when (other) {
is WebSymbolApiStatus.Obsolete,
is WebSymbolApiStatus.Deprecated,
is WebSymbolApiStatus.Experimental -> other
is WebSymbolApiStatus.Experimental,
-> other
else -> this
}
}
@@ -377,9 +384,11 @@ fun NavigationTarget.createPsiRangeNavigationItem(element: PsiElement, offsetWit
}
}
fun WebSymbolsScope.getDefaultCodeCompletions(qualifiedName: WebSymbolQualifiedName,
params: WebSymbolsCodeCompletionQueryParams,
scope: Stack<WebSymbolsScope>): List<WebSymbolCodeCompletionItem> =
fun WebSymbolsScope.getDefaultCodeCompletions(
qualifiedName: WebSymbolQualifiedName,
params: WebSymbolsCodeCompletionQueryParams,
scope: Stack<WebSymbolsScope>,
): List<WebSymbolCodeCompletionItem> =
getSymbols(qualifiedName.qualifiedKind,
WebSymbolsListSymbolsQueryParams.create(
params.queryExecutor,
@@ -391,7 +400,8 @@ fun WebSymbolsScope.getDefaultCodeCompletions(qualifiedName: WebSymbolQualifiedN
internal val List<WebSymbolsScope>.lastWebSymbol: WebSymbol?
get() = this.lastOrNull { it is WebSymbol } as? WebSymbol
internal fun createModificationTracker(trackersPointers: List<Pointer<out ModificationTracker>>): ModificationTracker =
@ApiStatus.Internal
fun createModificationTracker(trackersPointers: List<Pointer<out ModificationTracker>>): ModificationTracker =
ModificationTracker {
var modCount = 0L
for (tracker in trackersPointers) {

View File

@@ -10,7 +10,7 @@ import com.intellij.webSymbols.completion.WebSymbolCodeCompletionItem
import com.intellij.webSymbols.query.WebSymbolsQueryExecutor
import com.intellij.webSymbols.webTypes.filters.WebSymbolsFilter
class WebSymbolsFilterEP internal constructor() : CustomLoadingExtensionPointBean<WebSymbolsFilter>() {
class WebSymbolsFilterEP() : CustomLoadingExtensionPointBean<WebSymbolsFilter>() {
companion object {
private val EP_NAME = ExtensionPointName<WebSymbolsFilterEP>("com.intellij.webSymbols.webTypes.filter")
@@ -20,16 +20,20 @@ class WebSymbolsFilterEP internal constructor() : CustomLoadingExtensionPointBea
?: NOOP_FILTER
private val NOOP_FILTER = object : WebSymbolsFilter {
override fun filterCodeCompletions(codeCompletions: List<WebSymbolCodeCompletionItem>,
queryExecutor: WebSymbolsQueryExecutor,
scope: List<WebSymbolsScope>,
properties: Map<String, Any>): List<WebSymbolCodeCompletionItem> =
override fun filterCodeCompletions(
codeCompletions: List<WebSymbolCodeCompletionItem>,
queryExecutor: WebSymbolsQueryExecutor,
scope: List<WebSymbolsScope>,
properties: Map<String, Any>,
): List<WebSymbolCodeCompletionItem> =
codeCompletions
override fun filterNameMatches(matches: List<WebSymbol>,
queryExecutor: WebSymbolsQueryExecutor,
scope: List<WebSymbolsScope>,
properties: Map<String, Any>): List<WebSymbol> =
override fun filterNameMatches(
matches: List<WebSymbol>,
queryExecutor: WebSymbolsQueryExecutor,
scope: List<WebSymbolsScope>,
properties: Map<String, Any>,
): List<WebSymbol> =
matches
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.webSymbols
import com.intellij.psi.PsiElement
@@ -46,8 +46,10 @@ open class DebugOutputPrinter {
protected open fun printRecursiveValue(builder: StringBuilder, level: Int, value: Any) =
builder.append("<recursive value of class ${value.javaClass.simpleName}>")
protected open fun StringBuilder.printMap(level: Int,
map: Map<*, *>): StringBuilder =
protected open fun StringBuilder.printMap(
level: Int,
map: Map<*, *>,
): StringBuilder =
printObject(level) {
for (entry in map) {
printProperty(it, entry.key.toString(), entry.value)
@@ -69,8 +71,10 @@ open class DebugOutputPrinter {
return this
}
protected open fun StringBuilder.printObject(level: Int,
printer: (level: Int) -> Unit): StringBuilder {
protected open fun StringBuilder.printObject(
level: Int,
printer: (level: Int) -> Unit,
): StringBuilder {
append("{\n")
printer(level + 1)
indent(level).append("}")

View File

@@ -0,0 +1,663 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("WebTestUtil")
package com.intellij.webSymbols
import com.intellij.codeInsight.CodeInsightSettings
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.navigation.actions.GotoDeclarationOrUsageHandler2
import com.intellij.codeInsight.navigation.actions.GotoDeclarationOrUsageHandler2.Companion.testGTDUOutcome
import com.intellij.find.usages.api.SearchTarget
import com.intellij.find.usages.api.UsageOptions
import com.intellij.find.usages.impl.AllSearchOptions
import com.intellij.find.usages.impl.buildUsageViewQuery
import com.intellij.injected.editor.DocumentWindow
import com.intellij.injected.editor.EditorWindow
import com.intellij.lang.documentation.ide.IdeDocumentationTargetProvider
import com.intellij.lang.injection.InjectedLanguageManager
import com.intellij.model.psi.PsiSymbolReference
import com.intellij.model.psi.impl.referencesAt
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.backend.documentation.impl.computeDocumentationBlocking
import com.intellij.platform.testFramework.core.FileComparisonFailedError
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiPolyVariantReference
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.SearchScope
import com.intellij.psi.util.elementsAtOffsetUp
import com.intellij.refactoring.rename.api.RenameTarget
import com.intellij.refactoring.rename.symbol.SymbolRenameTargetFactory
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.TestDataFile
import com.intellij.testFramework.UsefulTestCase
import com.intellij.testFramework.UsefulTestCase.assertEmpty
import com.intellij.testFramework.assertInstanceOf
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.testFramework.fixtures.TestLookupElementPresentation
import com.intellij.usages.Usage
import com.intellij.util.ObjectUtils.coalesce
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.webSymbols.declarations.WebSymbolDeclaration
import com.intellij.webSymbols.declarations.WebSymbolDeclarationProvider
import com.intellij.webSymbols.impl.canUnwrapSymbols
import com.intellij.webSymbols.query.WebSymbolMatch
import com.intellij.webSymbols.query.WebSymbolsQueryExecutorFactory
import junit.framework.TestCase.*
import org.junit.Assert
import java.io.File
import java.util.concurrent.Callable
import kotlin.math.max
import kotlin.math.min
fun UsefulTestCase.enableAstLoadingFilter() {
Registry.get("ast.loading.filter").setValue(true, testRootDisposable)
}
fun UsefulTestCase.enableIdempotenceChecksOnEveryCache() {
Registry.get("platform.random.idempotence.check.rate").setValue(1, testRootDisposable)
}
fun <T> noAutoComplete(code: () -> T): T {
val old = CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION
CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION = false
try {
return code()
}
finally {
CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION = old
}
}
fun CodeInsightTestFixture.checkNoDocumentationAtCaret() {
assertNull(renderDocAtCaret())
}
fun CodeInsightTestFixture.checkDocumentationAtCaret() {
checkDocumentation(renderDocAtCaret())
}
fun CodeInsightTestFixture.checkLookupItems(
renderPriority: Boolean = false,
renderTypeText: Boolean = false,
renderTailText: Boolean = false,
renderProximity: Boolean = false,
renderDisplayText: Boolean = false,
renderDisplayEffects: Boolean = renderPriority,
checkDocumentation: Boolean = false,
containsCheck: Boolean = false,
locations: List<String> = emptyList(),
fileName: String = InjectedLanguageManager.getInstance(project).getTopLevelFile(file).virtualFile.nameWithoutExtension,
expectedDataLocation: String = "",
lookupItemFilter: (item: LookupElementInfo) -> Boolean = { true },
) {
val hasDir = expectedDataLocation.isNotEmpty()
fun checkLookupDocumentation(fileSuffix: String = "") {
if (!checkDocumentation) return
val lookupsToCheck = renderLookupItems(false, false, lookupFilter = lookupItemFilter).filter { it.isNotBlank() }
val lookupElements = lookupElements!!.asSequence().associateBy { it.lookupString }
for (lookupString in lookupsToCheck) {
val lookupElement = lookupElements[lookupString]
assertNotNull("Missing lookup string: $lookupString", lookupElement)
val doc = IdeDocumentationTargetProvider.getInstance(project)
.documentationTargets(editor, file, lookupElement!!)
?.firstOrNull()
?.let { computeDocumentationBlocking(it.createPointer()) }
?.html
?.trim()
val sanitizedLookupString = lookupString.replace(Regex("[*\"?<>/\\[\\]:;|,#]"), "_")
checkDocumentation(doc ?: "<no documentation>", "$fileSuffix#$sanitizedLookupString", expectedDataLocation)
}
}
noAutoComplete {
if (locations.isEmpty()) {
completeBasic()
checkListByFile(
renderLookupItems(renderPriority, renderTypeText, renderTailText, renderProximity, renderDisplayText, renderDisplayEffects,
lookupItemFilter),
expectedDataLocation + (if (hasDir) "/items" else "$fileName.items") + ".txt",
containsCheck
)
checkLookupDocumentation()
}
else {
locations.forEachIndexed { index, location ->
moveToOffsetBySignature(location)
completeBasic()
try {
checkListByFile(
renderLookupItems(renderPriority, renderTypeText, renderTailText, renderProximity, renderDisplayText, renderDisplayEffects,
lookupItemFilter),
expectedDataLocation + (if (hasDir) "/items" else "$fileName.items") + ".${index + 1}.txt",
containsCheck
)
}
catch (e: FileComparisonFailedError) {
throw FileComparisonFailedError(e.message + "\nFor location: $location",
e.expectedStringPresentation, e.actualStringPresentation,
e.filePath, e.actualFilePath)
}
checkLookupDocumentation(".${index + 1}")
}
}
}
}
data class LookupElementInfo(
val lookupElement: LookupElement,
val lookupString: String,
val displayText: String?,
val tailText: String?,
val typeText: String?,
val priority: Double,
val proximity: Int?,
val isStrikeout: Boolean,
val isItemTextBold: Boolean,
val isItemTextItalic: Boolean,
val isItemTextUnderline: Boolean,
val isTypeGreyed: Boolean,
) {
fun render(
renderPriority: Boolean,
renderTypeText: Boolean,
renderTailText: Boolean,
renderProximity: Boolean,
renderDisplayText: Boolean,
renderDisplayEffects: Boolean,
): String {
val result = StringBuilder()
result.append(lookupString)
if (renderPriority || renderTypeText || renderTailText || renderProximity || renderDisplayText || renderDisplayEffects) {
result.append(" (")
fun renderIf(switch: Boolean, name: String, value: String?) {
if (switch) {
if (result.last() == ';') {
result.append(" ")
}
result.append(name).append("=").append(value?.let { "'$it'" } ?: "null").append(";")
}
}
fun renderIf(switch: Boolean, name: String, value: Number?) {
if (switch) {
if (result.last() == ';') {
result.append(" ")
}
result.append(name).append("=").append(value ?: "null").append(";")
}
}
fun renderIf(switch: Boolean, name: String) {
if (switch) {
if (result.last() == ';') {
result.append(" ")
}
result.append(name).append(";")
}
}
renderIf(renderDisplayText, "displayText", displayText)
renderIf(renderTailText, "tailText", tailText)
renderIf(renderTypeText, "typeText", typeText)
renderIf(renderPriority, "priority", priority)
renderIf(renderProximity, "proximity", proximity)
if (renderDisplayEffects) {
renderIf(isStrikeout, "strikeout")
renderIf(isItemTextBold, "bold")
renderIf(isItemTextItalic, "italic")
renderIf(isItemTextUnderline, "underline")
renderIf(renderTypeText && isTypeGreyed, "typeGreyed")
}
if (result.last() == ';') {
result.setLength(result.length - 1)
}
result.append(")")
}
return result.toString()
}
}
private fun CodeInsightTestFixture.checkDocumentation(
actualDocumentation: String?,
fileSuffix: String = ".expected",
directory: String = "",
) {
assertNotNull("No documentation rendered", actualDocumentation)
val expectedFile = InjectedLanguageManager.getInstance(project).getTopLevelFile(file)
.virtualFile.nameWithoutExtension + fileSuffix + ".html"
val path = "$testDataPath/$directory/$expectedFile"
val file = File(path)
if (!file.exists()) {
file.createNewFile()
Logger.getInstance("DocumentationTest").warn("File $file not found - created!")
}
val expectedDocumentation = FileUtil.loadFile(file, "UTF-8", true).trim()
if (expectedDocumentation != actualDocumentation) {
throw FileComparisonFailedError(expectedFile, expectedDocumentation, actualDocumentation!!, path)
}
}
private fun CodeInsightTestFixture.renderDocAtCaret(): String? =
IdeDocumentationTargetProvider.getInstance(project)
.documentationTargets(editor, file, caretOffset)
.mapNotNull { computeDocumentationBlocking(it.createPointer())?.html }
.also { assertTrue("More then one documentation rendered:\n\n${it.joinToString("\n\n")}", it.size <= 1) }
.getOrNull(0)
?.trim()
?.replace(Regex("<a href=\"psi_element:[^\"]*/unitTest[0-9]+/"), "<a href=\"psi_element:///src/")
infix fun ((item: LookupElementInfo) -> Boolean).and(other: (item: LookupElementInfo) -> Boolean): (item: LookupElementInfo) -> Boolean =
{
this(it) && other(it)
}
@JvmOverloads
fun CodeInsightTestFixture.renderLookupItems(
renderPriority: Boolean,
renderTypeText: Boolean,
renderTailText: Boolean = false,
renderProximity: Boolean = false,
renderDisplayText: Boolean = false,
renderDisplayEffects: Boolean = renderPriority,
lookupFilter: (item: LookupElementInfo) -> Boolean = { true },
): List<String> =
lookupElements?.asSequence()
?.map {
val presentation = TestLookupElementPresentation.renderReal(it)
LookupElementInfo(it, it.lookupString, presentation.itemText, presentation.tailText,
presentation.typeText, (it as? PrioritizedLookupElement<*>)?.priority ?: 0.0,
(it as? PrioritizedLookupElement<*>)?.explicitProximity,
presentation.isStrikeout, presentation.isItemTextBold,
presentation.isItemTextItalic, presentation.isItemTextUnderlined,
presentation.isTypeGrayed)
}
?.filter(lookupFilter)
?.sortedWith(
Comparator.comparing { it: LookupElementInfo -> -it.priority }
.thenComparingInt { -(it.proximity ?: 0) }
.thenComparing { it: LookupElementInfo -> it.lookupString })
?.map {
it.render(
renderPriority = renderPriority,
renderTypeText = renderTypeText,
renderTailText = renderTailText,
renderProximity = renderProximity,
renderDisplayText = renderDisplayText,
renderDisplayEffects = renderDisplayEffects,
)
}
?.toList()
?: emptyList()
fun CodeInsightTestFixture.moveToOffsetBySignature(signature: String) {
PsiDocumentManager.getInstance(project).commitAllDocuments()
val offset = file.findOffsetBySignature(signature)
editor.caretModel.moveToOffset(offset)
}
fun PsiFile.findOffsetBySignature(signature: String): Int {
var str = signature
val caretSignature = "<caret>"
val caretOffset = str.indexOf(caretSignature)
assert(caretOffset >= 0) { "Caret offset is required" }
str = str.substring(0, caretOffset) + str.substring(caretOffset + caretSignature.length)
val pos = text.indexOf(str)
assertTrue("Failed to locate '$str' in: \n $text", pos >= 0)
return pos + caretOffset
}
fun CodeInsightTestFixture.webSymbolAtCaret(): WebSymbol? =
injectionThenHost(file, caretOffset) { file, offset ->
file.webSymbolDeclarationsAt(offset).takeIf { it.isNotEmpty() }
?: if (offset > 0) file.webSymbolDeclarationsAt(offset - 1).takeIf { it.isNotEmpty() } else null
}
?.takeIf { it.isNotEmpty() }
?.also { if (it.size > 1) throw AssertionError("Multiple WebSymbolDeclarations found at caret position: $it") }
?.firstOrNull()
?.symbol
?: injectionThenHost(file, caretOffset) { file, offset ->
file.referencesAt(offset).filter { it.absoluteRange.contains(offset) }.takeIf { it.isNotEmpty() }
?: if (offset > 0) file.referencesAt(offset - 1).filter { it.absoluteRange.contains(offset - 1) }.takeIf { it.isNotEmpty() } else null
}
?.also { if (it.size > 1) throw AssertionError("Multiple PsiSymbolReferences found at caret position: $it") }
?.resolveToWebSymbols()
?.also {
if (it.size > 1) {
throw AssertionError("More than one symbol at caret position: $it")
}
}
?.getOrNull(0)
fun CodeInsightTestFixture.webSymbolSourceAtCaret(): PsiElement? =
webSymbolAtCaret()?.let { it as? PsiSourcedWebSymbol }?.source
fun CodeInsightTestFixture.resolveWebSymbolReference(signature: String): WebSymbol {
val symbols = multiResolveWebSymbolReference(signature)
if (symbols.isEmpty()) {
throw AssertionError("Reference resolves to null at '$signature'")
}
if (symbols.size != 1) {
throw AssertionError("Reference resolves to more than one element at '" + signature + "': "
+ symbols)
}
return symbols[0]
}
fun CodeInsightTestFixture.multiResolveWebSymbolReference(signature: String): List<WebSymbol> {
val signatureOffset = file.findOffsetBySignature(signature)
return injectionThenHost(file, signatureOffset) { file, offset ->
file.referencesAt(offset)
.let { refs ->
if (refs.size > 1) {
val filtered = refs.filter { it.absoluteRange.contains(signatureOffset) }
if (filtered.size == 1)
filtered
else throw AssertionError("Multiple PsiSymbolReferences found at $signature: $refs")
}
else refs
}
.resolveToWebSymbols()
.takeIf { it.isNotEmpty() }
} ?: emptyList()
}
private fun Collection<PsiSymbolReference>.resolveToWebSymbols(): List<WebSymbol> =
asSequence()
.flatMap { it.resolveReference() }
.filterIsInstance<WebSymbol>()
.flatMap {
if (it is WebSymbolMatch
&& it.nameSegments.size == 1
&& it.nameSegments[0].canUnwrapSymbols()) {
it.nameSegments[0].symbols
}
else listOf(it)
}
.toList()
private fun PsiFile.webSymbolDeclarationsAt(offset: Int): Collection<WebSymbolDeclaration> {
for ((element, offsetInElement) in elementsAtOffsetUp(offset)) {
val declarations = WebSymbolDeclarationProvider.getAllDeclarations(element, offsetInElement)
if (declarations.isNotEmpty()) {
return declarations
}
}
return emptyList()
}
private fun <T> injectionThenHost(file: PsiFile, offset: Int, computation: (PsiFile, Int) -> T?): T? {
val injectedFile = InjectedLanguageManager.getInstance(file.project).findInjectedElementAt(file, offset)?.containingFile
if (injectedFile != null) {
PsiDocumentManager.getInstance(file.project).getDocument(injectedFile)
?.let { it as? DocumentWindow }
?.hostToInjected(offset)
?.takeIf { it >= 0 }
?.let { computation(injectedFile, it) }
?.let { return it }
}
return computation(file, offset)
}
fun CodeInsightTestFixture.resolveToWebSymbolSource(signature: String): PsiElement {
val webSymbol = resolveWebSymbolReference(signature)
val result = assertInstanceOf<PsiSourcedWebSymbol>(webSymbol).source
assertNotNull("WebSymbol $webSymbol source is null", result)
return result!!
}
fun CodeInsightTestFixture.resolveReference(signature: String): PsiElement {
val offsetBySignature = file.findOffsetBySignature(signature)
var ref = file.findReferenceAt(offsetBySignature)
if (ref === null) {
//possibly an injection
ref = InjectedLanguageManager.getInstance(project)
.findInjectedElementAt(file, offsetBySignature)
?.findReferenceAt(0)
}
assertNotNull("No reference at '$signature'", ref)
var resolve = ref!!.resolve()
if (resolve == null && ref is PsiPolyVariantReference) {
val results = ref.multiResolve(false).filter { it.isValidResult }
if (results.size > 1) {
throw AssertionError("Reference resolves to more than one element at '" + signature + "': "
+ results)
}
else if (results.size == 1) {
resolve = results[0].element
}
}
assertNotNull("Reference resolves to null at '$signature'", resolve)
return resolve!!
}
fun CodeInsightTestFixture.multiResolveReference(signature: String): List<PsiElement> {
val offsetBySignature = file.findOffsetBySignature(signature)
val ref = file.findReferenceAt(offsetBySignature)
assertNotNull("No reference at '$signature'", ref)
assertTrue("PsiPolyVariantReference expected", ref is PsiPolyVariantReference)
val resolveResult = (ref as PsiPolyVariantReference).multiResolve(false)
assertFalse("Empty reference resolution at '$signature'", resolveResult.isEmpty())
return resolveResult.mapNotNull { it.element }
}
@JvmOverloads
fun CodeInsightTestFixture.assertUnresolvedReference(signature: String, okWithNoRef: Boolean = false, allowSelfReference: Boolean = false) {
assertEmpty("Reference at $signature should not resolve to WebSymbols.", multiResolveWebSymbolReference(signature))
val offsetBySignature = file.findOffsetBySignature(signature)
val ref = file.findReferenceAt(offsetBySignature)
if (okWithNoRef && ref == null) {
return
}
assertNotNull("Expected not null reference for signature '$signature' at offset $offsetBySignature in file\n${file.text}", ref)
val resolved = ref!!.resolve()
if (ref.element == resolved && allowSelfReference) {
if (ref is PsiPolyVariantReference) {
assertEmpty(ref.multiResolve(false).filter { it.element != ref.element })
}
return
}
assertNull(
"Expected that reference for signature '$signature' at offset $offsetBySignature resolves to null but resolved to $resolved (${resolved?.text}) in file ${resolved?.containingFile?.name}",
resolved)
if (ref is PsiPolyVariantReference) {
assertEmpty(ref.multiResolve(false))
}
}
fun CodeInsightTestFixture.findUsages(target: SearchTarget): MutableCollection<out Usage> {
val project = project
val searchScope = coalesce<SearchScope>(target.maximalSearchScope, GlobalSearchScope.allScope(project))
val allOptions = AllSearchOptions(UsageOptions.createOptions(searchScope), true)
return buildUsageViewQuery(getProject(), target, allOptions).findAll()
}
@JvmOverloads
@RequiresEdt
fun CodeInsightTestFixture.checkGTDUOutcome(expectedOutcome: GotoDeclarationOrUsageHandler2.GTDUOutcome?, signature: String? = null) {
if (signature != null) {
moveToOffsetBySignature(signature)
}
val actualSignature = signature ?: editor.currentPositionSignature
val editor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, file)
val offset = editor.caretModel.offset
val gtduOutcome = ReadAction
.nonBlocking(Callable {
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: file
testGTDUOutcome(editor, file, offset)
})
.submit(AppExecutorUtil.getAppExecutorService())
.get()
Assert.assertEquals(actualSignature,
expectedOutcome,
gtduOutcome)
}
fun CodeInsightTestFixture.checkGotoDeclaration(fromSignature: String?, declarationSignature: String, expectedFileName: String? = null) {
checkGTDUOutcome(GotoDeclarationOrUsageHandler2.GTDUOutcome.GTD, fromSignature)
val actualSignature = fromSignature ?: editor.currentPositionSignature
performEditorAction("GotoDeclaration")
val targetEditor = FileEditorManager.getInstance(project).selectedTextEditor?.topLevelEditor
if (targetEditor == null) throw NullPointerException(actualSignature)
val targetFile = PsiDocumentManager.getInstance(project).getPsiFile(targetEditor.document)!!
if (expectedFileName != null) {
assertEquals(actualSignature, expectedFileName, PsiDocumentManager.getInstance(project).getPsiFile(targetEditor.document)?.name)
}
else {
assertEquals(actualSignature, targetEditor, editor.topLevelEditor)
}
if (!declarationSignature.contains("<caret>") || targetFile.findOffsetBySignature(
declarationSignature) != targetEditor.caretModel.offset) {
assertEquals("For go to from: $actualSignature",
declarationSignature + if (!declarationSignature.contains("<caret>")) ""
else (" [" + file.findOffsetBySignature(declarationSignature) + "]"),
targetEditor.currentPositionSignature + "[${targetEditor.caretModel.offset}]")
}
}
fun CodeInsightTestFixture.checkListByFile(actualList: List<String>, @TestDataFile expectedFile: String, containsCheck: Boolean) {
val path = "$testDataPath/$expectedFile"
val file = File(path)
if (!file.exists() && file.createNewFile()) {
Logger.getInstance("#WebTestUtilKt").warn("File $file has been created.")
}
val actualContents = actualList.joinToString("\n").trim() + "\n"
val expectedContents = FileUtil.loadFile(file, "UTF-8", true).trim() + "\n"
if (containsCheck) {
val expectedList = FileUtil.loadLines(file, "UTF-8").filter { it.isNotBlank() }
val actualSet = actualList.toSet()
if (!expectedList.all { actualSet.contains(it) }) {
throw FileComparisonFailedError(expectedFile, expectedContents, actualContents, path)
}
}
else if (expectedContents != actualContents) {
throw FileComparisonFailedError(expectedFile, expectedContents, actualContents, path)
}
}
fun CodeInsightTestFixture.checkTextByFile(actualContents: String, @TestDataFile expectedFile: String) {
val path = "$testDataPath/$expectedFile"
val file = File(path)
if (!file.exists() && file.createNewFile()) {
Logger.getInstance("#WebTestUtilKt").warn("File $file has been created.")
}
val actualContentsTrimmed = actualContents.trim() + "\n"
val expectedContents = FileUtil.loadFile(file, "UTF-8", true).trim() + "\n"
if (expectedContents != actualContentsTrimmed) {
throw FileComparisonFailedError(expectedFile, expectedContents, actualContentsTrimmed, path)
}
}
fun CodeInsightTestFixture.canRenameWebSymbolAtCaret() =
webSymbolAtCaret().let {
it is RenameTarget || it?.renameTarget != null || (it is PsiSourcedWebSymbol && it.source != null)
}
fun CodeInsightTestFixture.renameWebSymbol(newName: String) {
val symbol = webSymbolAtCaret() ?: throw AssertionError("No WebSymbol at caret")
var target: RenameTarget? = null
for (factory: SymbolRenameTargetFactory in SymbolRenameTargetFactory.EP_NAME.extensions) {
target = factory.renameTarget(project, symbol)
if (target != null) break
}
if (target == null) {
target = when (symbol) {
is RenameTarget -> symbol
is PsiSourcedWebSymbol -> {
val psiTarget = symbol.source
?: throw AssertionError("Symbol $symbol provides null source")
renameElement(psiTarget, newName)
return
}
else -> symbol.renameTarget
?: throw AssertionError("Symbol $symbol does not provide rename target nor is a PsiSourcedWebSymbol")
}
}
if (target.createPointer().dereference() == null) {
throw AssertionError("Target $target pointer dereferences to null")
}
renameTarget(target, newName)
}
fun doCompletionItemsTest(
fixture: CodeInsightTestFixture,
fileName: String,
goldFileWithExtension: Boolean = false,
renderDisplayText: Boolean = false,
) {
val fileNameNoExt = FileUtil.getNameWithoutExtension(fileName)
fixture.configureByFile(fileName)
WriteAction.runAndWait<Throwable> { WebSymbolsQueryExecutorFactory.getInstance(fixture.project) }
val document = fixture.getDocument(fixture.file)
val offsets = mutableListOf<Pair<Int, Boolean>>()
WriteAction.runAndWait<Throwable> {
CommandProcessor.getInstance().executeCommand(fixture.project, {
val chars = document.charsSequence
var pos: Int
while (chars.indexOf('|').also { pos = it } >= 0) {
val strict = chars.length > pos + 1 && chars[pos + 1] == '!'
offsets.add(Pair(pos, strict))
if (strict)
document.deleteString(pos, pos + 2)
else
document.deleteString(pos, pos + 1)
}
}, null, null)
PsiDocumentManager.getInstance(fixture.project).commitDocument(document)
}
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
noAutoComplete {
offsets.forEachIndexed { index, (offset, strict) ->
fixture.editor.caretModel.moveToOffset(offset)
fixture.completeBasic()
fixture.checkListByFile(
fixture.renderLookupItems(true, true, true, renderDisplayText = renderDisplayText),
"gold/${if (goldFileWithExtension) fileName else fileNameNoExt}.${index}.txt", !strict)
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
}
}
}
private val Editor.currentPositionSignature: String
get() {
val caretPos = caretModel.offset
val text = document.text
return (text.substring(max(0, caretPos - 15), caretPos) + "<caret>" +
text.substring(caretPos, min(caretPos + 15, text.length)))
.replace("\n", "\\n")
}
private val Editor.topLevelEditor
get() = if (this is EditorWindow) delegate else this

View File

@@ -0,0 +1,134 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.webSymbols.query
import com.intellij.util.applyIf
import com.intellij.webSymbols.DebugOutputPrinter
import com.intellij.webSymbols.PsiSourcedWebSymbol
import com.intellij.webSymbols.WebSymbol
import com.intellij.webSymbols.WebSymbolApiStatus
import com.intellij.webSymbols.WebSymbolNameSegment
import com.intellij.webSymbols.completion.WebSymbolCodeCompletionItem
import com.intellij.webSymbols.html.WebSymbolHtmlAttributeValue
import com.intellij.webSymbols.utils.completeMatch
import com.intellij.webSymbols.utils.nameSegments
import java.util.Locale
import java.util.Stack
open class WebSymbolsDebugOutputPrinter : DebugOutputPrinter() {
private val parents = Stack<WebSymbol>()
override fun printValueImpl(builder: StringBuilder, level: Int, value: Any?): StringBuilder =
when (value) {
is WebSymbolCodeCompletionItem -> builder.printCodeCompletionItem(level, value)
is WebSymbol -> builder.printSymbol(level, value)
is WebSymbolHtmlAttributeValue -> builder.printAttributeValue(level, value)
is WebSymbolNameSegment -> builder.printSegment(level, value)
is WebSymbolApiStatus -> builder.printApiStatus(value)
is Set<*> -> builder.printSet(value)
else -> super.printValueImpl(builder, level, value)
}
override fun printRecursiveValue(builder: StringBuilder, level: Int, value: Any): StringBuilder =
if (value is WebSymbol)
if (parents.peek() == value) builder.append("<self>") else builder.append("<recursive>")
else
super.printRecursiveValue(builder, level, value)
private fun StringBuilder.printCodeCompletionItem(topLevel: Int, item: WebSymbolCodeCompletionItem): StringBuilder =
printObject(topLevel) { level ->
printProperty(level, "name", item.name)
printProperty(level, "priority", item.priority ?: WebSymbol.Priority.NORMAL)
printProperty(level, "proximity", item.proximity?.takeIf { it > 0 })
printProperty(level, "displayName", item.displayName.takeIf { it != item.name })
printProperty(level, "offset", item.offset.takeIf { it != 0 })
printProperty(level, "completeAfterInsert", item.completeAfterInsert.takeIf { it })
printProperty(level, "completeAfterChars", item.completeAfterChars.takeIf { it.isNotEmpty() })
printProperty(level, "aliases", item.aliases.takeIf { it.isNotEmpty() })
printProperty(level, "source", item.symbol)
}
private fun StringBuilder.printSet(set: Set<*>): StringBuilder {
return append(set.toString())
}
private fun StringBuilder.printSymbol(topLevel: Int, source: WebSymbol): StringBuilder {
if (parents.contains(source)) {
if (parents.peek() == source) append("<self>") else append("<recursive>")
return this
}
printObject(topLevel) { level ->
if (source.pattern != null) {
printProperty(level, "matchedName", source.namespace.lowercase(Locale.US) + "/" + source.kind + "/<pattern>")
printProperty(level, "name", source.name)
}
else {
printProperty(level, "matchedName", source.namespace.lowercase(Locale.US) + "/" + source.kind + "/" + source.name)
}
printProperty(level, "origin", "${source.origin.library}@${source.origin.version} (${source.origin.framework ?: "<none>"})")
printProperty(level, "source", (source as? PsiSourcedWebSymbol)?.source)
printProperty(level, "type", source.type)
printProperty(level, "attrValue", source.attributeValue)
printProperty(level, "complete", source.completeMatch)
printProperty(level, "description", source.description?.ellipsis(45))
printProperty(level, "docUrl", source.docUrl)
printProperty(level, "descriptionSections", source.descriptionSections.takeIf { it.isNotEmpty() })
printProperty(level, "abstract", source.abstract.takeIf { it })
printProperty(level, "virtual", source.virtual.takeIf { it })
printProperty(level, "apiStatus", source.apiStatus.takeIf { it !is WebSymbolApiStatus.Stable || it.since != null })
printProperty(level, "priority", source.priority ?: WebSymbol.Priority.NORMAL)
printProperty(level, "proximity", source.proximity?.takeIf { it > 0 })
printProperty(level, "has-pattern", if (source.pattern != null) true else null)
printProperty(level, "properties", source.properties.takeIf { it.isNotEmpty() })
parents.push(source)
printProperty(level, "segments", source.nameSegments)
parents.pop()
}
return this
}
private fun StringBuilder.printSegment(topLevel: Int,
segment: WebSymbolNameSegment
): StringBuilder =
printObject(topLevel) { level ->
printProperty(level, "name-part", parents.peek().let { if (it.pattern == null) segment.getName(parents.peek()) else "" })
printProperty(level, "display-name", segment.displayName)
printProperty(level, "apiStatus", segment.apiStatus)
printProperty(level, "priority", segment.priority?.takeIf { it != WebSymbol.Priority.NORMAL })
printProperty(level, "matchScore", segment.matchScore.takeIf { it != segment.end - segment.start })
printProperty(level, "problem", segment.problem)
val symbols = segment.symbols.filter { !it.extension }
if (symbols.size > 1) {
printProperty(level, "symbols", symbols)
}
else if (symbols.size == 1) {
printProperty(level, "symbol", symbols[0])
}
}
private fun StringBuilder.printAttributeValue(topLevel: Int, value: WebSymbolHtmlAttributeValue): StringBuilder =
printObject(topLevel) { level ->
printProperty(level, "kind", value.kind)
.printProperty(level, "type", value.type)
.printProperty(level, "langType", value.langType)
.printProperty(level, "required", value.required)
.printProperty(level, "default", value.default)
}
private fun StringBuilder.printApiStatus(apiStatus: WebSymbolApiStatus): StringBuilder =
when (apiStatus) {
is WebSymbolApiStatus.Deprecated -> append("deprecated")
.applyIf(apiStatus.since != null) { append(" in ").append(apiStatus.since) }
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
is WebSymbolApiStatus.Obsolete -> append("obsolete")
.applyIf(apiStatus.since != null) { append(" in ").append(apiStatus.since) }
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
is WebSymbolApiStatus.Experimental -> append("experimental")
.applyIf(apiStatus.since != null) { append(" since ").append(apiStatus.since) }
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
is WebSymbolApiStatus.Stable -> append("stable")
.applyIf(apiStatus.since != null) { append(" since ").append(apiStatus.since) }
}
}

View File

@@ -1,666 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("WebTestUtil")
package com.intellij.webSymbols
import com.intellij.codeInsight.CodeInsightSettings
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.navigation.actions.GotoDeclarationOrUsageHandler2
import com.intellij.codeInsight.navigation.actions.GotoDeclarationOrUsageHandler2.Companion.testGTDUOutcome
import com.intellij.find.usages.api.SearchTarget
import com.intellij.find.usages.api.UsageOptions
import com.intellij.find.usages.impl.AllSearchOptions
import com.intellij.find.usages.impl.buildUsageViewQuery
import com.intellij.injected.editor.DocumentWindow
import com.intellij.injected.editor.EditorWindow
import com.intellij.lang.documentation.ide.IdeDocumentationTargetProvider
import com.intellij.lang.injection.InjectedLanguageManager
import com.intellij.model.psi.PsiSymbolReference
import com.intellij.model.psi.impl.referencesAt
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.backend.documentation.impl.computeDocumentationBlocking
import com.intellij.platform.testFramework.core.FileComparisonFailedError
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiPolyVariantReference
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.SearchScope
import com.intellij.psi.util.elementsAtOffsetUp
import com.intellij.refactoring.rename.api.RenameTarget
import com.intellij.refactoring.rename.symbol.SymbolRenameTargetFactory
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.TestDataFile
import com.intellij.testFramework.UsefulTestCase
import com.intellij.testFramework.UsefulTestCase.assertEmpty
import com.intellij.testFramework.assertInstanceOf
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.testFramework.fixtures.TestLookupElementPresentation
import com.intellij.usages.Usage
import com.intellij.util.ObjectUtils.coalesce
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.webSymbols.declarations.WebSymbolDeclaration
import com.intellij.webSymbols.declarations.WebSymbolDeclarationProvider
import com.intellij.webSymbols.impl.canUnwrapSymbols
import com.intellij.webSymbols.query.WebSymbolMatch
import com.intellij.webSymbols.query.WebSymbolsQueryExecutorFactory
import junit.framework.TestCase.*
import org.junit.Assert
import org.opentest4j.AssertionFailedError
import java.io.File
import java.util.concurrent.Callable
import kotlin.math.max
import kotlin.math.min
internal val webSymbolsTestsDataPath: String
get() = "${PlatformTestUtil.getCommunityPath()}/platform/webSymbols/testData/"
fun UsefulTestCase.enableAstLoadingFilter() {
Registry.get("ast.loading.filter").setValue(true, testRootDisposable)
}
fun UsefulTestCase.enableIdempotenceChecksOnEveryCache() {
Registry.get("platform.random.idempotence.check.rate").setValue(1, testRootDisposable)
}
fun <T> noAutoComplete(code: () -> T): T {
val old = CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION
CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION = false
try {
return code()
}
finally {
CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION = old
}
}
fun CodeInsightTestFixture.checkNoDocumentationAtCaret() {
assertNull(renderDocAtCaret())
}
fun CodeInsightTestFixture.checkDocumentationAtCaret() {
checkDocumentation(renderDocAtCaret())
}
fun CodeInsightTestFixture.checkLookupItems(
renderPriority: Boolean = false,
renderTypeText: Boolean = false,
renderTailText: Boolean = false,
renderProximity: Boolean = false,
renderDisplayText: Boolean = false,
renderDisplayEffects: Boolean = renderPriority,
checkDocumentation: Boolean = false,
containsCheck: Boolean = false,
locations: List<String> = emptyList(),
fileName: String = InjectedLanguageManager.getInstance(project).getTopLevelFile(file).virtualFile.nameWithoutExtension,
expectedDataLocation: String = "",
lookupItemFilter: (item: LookupElementInfo) -> Boolean = { true },
) {
val hasDir = expectedDataLocation.isNotEmpty()
fun checkLookupDocumentation(fileSuffix: String = "") {
if (!checkDocumentation) return
val lookupsToCheck = renderLookupItems(false, false, lookupFilter = lookupItemFilter).filter { it.isNotBlank() }
val lookupElements = lookupElements!!.asSequence().associateBy { it.lookupString }
for (lookupString in lookupsToCheck) {
val lookupElement = lookupElements[lookupString]
assertNotNull("Missing lookup string: $lookupString", lookupElement)
val doc = IdeDocumentationTargetProvider.getInstance(project)
.documentationTargets(editor, file, lookupElement!!)
?.firstOrNull()
?.let { computeDocumentationBlocking(it.createPointer()) }
?.html
?.trim()
val sanitizedLookupString = lookupString.replace(Regex("[*\"?<>/\\[\\]:;|,#]"), "_")
checkDocumentation(doc ?: "<no documentation>", "$fileSuffix#$sanitizedLookupString", expectedDataLocation)
}
}
noAutoComplete {
if (locations.isEmpty()) {
completeBasic()
checkListByFile(
renderLookupItems(renderPriority, renderTypeText, renderTailText, renderProximity, renderDisplayText, renderDisplayEffects,
lookupItemFilter),
expectedDataLocation + (if (hasDir) "/items" else "$fileName.items") + ".txt",
containsCheck
)
checkLookupDocumentation()
}
else {
locations.forEachIndexed { index, location ->
moveToOffsetBySignature(location)
completeBasic()
try {
checkListByFile(
renderLookupItems(renderPriority, renderTypeText, renderTailText, renderProximity, renderDisplayText, renderDisplayEffects,
lookupItemFilter),
expectedDataLocation + (if (hasDir) "/items" else "$fileName.items") + ".${index + 1}.txt",
containsCheck
)
} catch (e: FileComparisonFailedError) {
throw FileComparisonFailedError(e.message + "\nFor location: $location",
e.expectedStringPresentation, e.actualStringPresentation,
e.filePath, e.actualFilePath)
}
checkLookupDocumentation(".${index + 1}")
}
}
}
}
data class LookupElementInfo(
val lookupElement: LookupElement,
val lookupString: String,
val displayText: String?,
val tailText: String?,
val typeText: String?,
val priority: Double,
val proximity: Int?,
val isStrikeout: Boolean,
val isItemTextBold: Boolean,
val isItemTextItalic: Boolean,
val isItemTextUnderline: Boolean,
val isTypeGreyed: Boolean,
) {
fun render(
renderPriority: Boolean,
renderTypeText: Boolean,
renderTailText: Boolean,
renderProximity: Boolean,
renderDisplayText: Boolean,
renderDisplayEffects: Boolean,
): String {
val result = StringBuilder()
result.append(lookupString)
if (renderPriority || renderTypeText || renderTailText || renderProximity || renderDisplayText || renderDisplayEffects) {
result.append(" (")
fun renderIf(switch: Boolean, name: String, value: String?) {
if (switch) {
if (result.last() == ';') {
result.append(" ")
}
result.append(name).append("=").append(value?.let { "'$it'" } ?: "null").append(";")
}
}
fun renderIf(switch: Boolean, name: String, value: Number?) {
if (switch) {
if (result.last() == ';') {
result.append(" ")
}
result.append(name).append("=").append(value ?: "null").append(";")
}
}
fun renderIf(switch: Boolean, name: String) {
if (switch) {
if (result.last() == ';') {
result.append(" ")
}
result.append(name).append(";")
}
}
renderIf(renderDisplayText, "displayText", displayText)
renderIf(renderTailText, "tailText", tailText)
renderIf(renderTypeText, "typeText", typeText)
renderIf(renderPriority, "priority", priority)
renderIf(renderProximity, "proximity", proximity)
if (renderDisplayEffects) {
renderIf(isStrikeout, "strikeout")
renderIf(isItemTextBold, "bold")
renderIf(isItemTextItalic, "italic")
renderIf(isItemTextUnderline, "underline")
renderIf(renderTypeText && isTypeGreyed, "typeGreyed")
}
if (result.last() == ';') {
result.setLength(result.length - 1)
}
result.append(")")
}
return result.toString()
}
}
private fun CodeInsightTestFixture.checkDocumentation(
actualDocumentation: String?,
fileSuffix: String = ".expected",
directory: String = "",
) {
assertNotNull("No documentation rendered", actualDocumentation)
val expectedFile = InjectedLanguageManager.getInstance(project).getTopLevelFile(file)
.virtualFile.nameWithoutExtension + fileSuffix + ".html"
val path = "$testDataPath/$directory/$expectedFile"
val file = File(path)
if (!file.exists()) {
file.createNewFile()
Logger.getInstance("DocumentationTest").warn("File $file not found - created!")
}
val expectedDocumentation = FileUtil.loadFile(file, "UTF-8", true).trim()
if (expectedDocumentation != actualDocumentation) {
throw FileComparisonFailedError(expectedFile, expectedDocumentation, actualDocumentation!!, path)
}
}
private fun CodeInsightTestFixture.renderDocAtCaret(): String? =
IdeDocumentationTargetProvider.getInstance(project)
.documentationTargets(editor, file, caretOffset)
.mapNotNull { computeDocumentationBlocking(it.createPointer())?.html }
.also { assertTrue("More then one documentation rendered:\n\n${it.joinToString("\n\n")}", it.size <= 1) }
.getOrNull(0)
?.trim()
?.replace(Regex("<a href=\"psi_element:[^\"]*/unitTest[0-9]+/"), "<a href=\"psi_element:///src/")
infix fun ((item: LookupElementInfo) -> Boolean).and(other: (item: LookupElementInfo) -> Boolean): (item: LookupElementInfo) -> Boolean =
{
this(it) && other(it)
}
@JvmOverloads
fun CodeInsightTestFixture.renderLookupItems(
renderPriority: Boolean,
renderTypeText: Boolean,
renderTailText: Boolean = false,
renderProximity: Boolean = false,
renderDisplayText: Boolean = false,
renderDisplayEffects: Boolean = renderPriority,
lookupFilter: (item: LookupElementInfo) -> Boolean = { true },
): List<String> =
lookupElements?.asSequence()
?.map {
val presentation = TestLookupElementPresentation.renderReal(it)
LookupElementInfo(it, it.lookupString, presentation.itemText, presentation.tailText,
presentation.typeText, (it as? PrioritizedLookupElement<*>)?.priority ?: 0.0,
(it as? PrioritizedLookupElement<*>)?.explicitProximity,
presentation.isStrikeout, presentation.isItemTextBold,
presentation.isItemTextItalic, presentation.isItemTextUnderlined,
presentation.isTypeGrayed)
}
?.filter(lookupFilter)
?.sortedWith(
Comparator.comparing { it: LookupElementInfo -> -it.priority }
.thenComparingInt { -(it.proximity ?: 0) }
.thenComparing { it: LookupElementInfo -> it.lookupString })
?.map {
it.render(
renderPriority = renderPriority,
renderTypeText = renderTypeText,
renderTailText = renderTailText,
renderProximity = renderProximity,
renderDisplayText = renderDisplayText,
renderDisplayEffects = renderDisplayEffects,
)
}
?.toList()
?: emptyList()
fun CodeInsightTestFixture.moveToOffsetBySignature(signature: String) {
PsiDocumentManager.getInstance(project).commitAllDocuments()
val offset = file.findOffsetBySignature(signature)
editor.caretModel.moveToOffset(offset)
}
fun PsiFile.findOffsetBySignature(signature: String): Int {
var str = signature
val caretSignature = "<caret>"
val caretOffset = str.indexOf(caretSignature)
assert(caretOffset >= 0) { "Caret offset is required" }
str = str.substring(0, caretOffset) + str.substring(caretOffset + caretSignature.length)
val pos = text.indexOf(str)
assertTrue("Failed to locate '$str' in: \n $text", pos >= 0)
return pos + caretOffset
}
fun CodeInsightTestFixture.webSymbolAtCaret(): WebSymbol? =
injectionThenHost(file, caretOffset) { file, offset ->
file.webSymbolDeclarationsAt(offset).takeIf { it.isNotEmpty() }
?: if (offset > 0) file.webSymbolDeclarationsAt(offset - 1).takeIf { it.isNotEmpty() } else null
}
?.takeIf { it.isNotEmpty() }
?.also { if (it.size > 1) throw AssertionError("Multiple WebSymbolDeclarations found at caret position: $it") }
?.firstOrNull()
?.symbol
?: injectionThenHost(file, caretOffset) { file, offset ->
file.referencesAt(offset).filter { it.absoluteRange.contains(offset) }.takeIf { it.isNotEmpty() }
?: if (offset > 0) file.referencesAt(offset - 1).filter { it.absoluteRange.contains(offset - 1) }.takeIf { it.isNotEmpty() } else null
}
?.also { if (it.size > 1) throw AssertionError("Multiple PsiSymbolReferences found at caret position: $it") }
?.resolveToWebSymbols()
?.also {
if (it.size > 1) {
throw AssertionError("More than one symbol at caret position: $it")
}
}
?.getOrNull(0)
fun CodeInsightTestFixture.webSymbolSourceAtCaret(): PsiElement? =
webSymbolAtCaret()?.let { it as? PsiSourcedWebSymbol }?.source
fun CodeInsightTestFixture.resolveWebSymbolReference(signature: String): WebSymbol {
val symbols = multiResolveWebSymbolReference(signature)
if (symbols.isEmpty()) {
throw AssertionError("Reference resolves to null at '$signature'")
}
if (symbols.size != 1) {
throw AssertionError("Reference resolves to more than one element at '" + signature + "': "
+ symbols)
}
return symbols[0]
}
fun CodeInsightTestFixture.multiResolveWebSymbolReference(signature: String): List<WebSymbol> {
val signatureOffset = file.findOffsetBySignature(signature)
return injectionThenHost(file, signatureOffset) { file, offset ->
file.referencesAt(offset)
.let { refs ->
if (refs.size > 1) {
val filtered = refs.filter { it.absoluteRange.contains(signatureOffset) }
if (filtered.size == 1)
filtered
else throw AssertionError("Multiple PsiSymbolReferences found at $signature: $refs")
}
else refs
}
.resolveToWebSymbols()
.takeIf { it.isNotEmpty() }
} ?: emptyList()
}
private fun Collection<PsiSymbolReference>.resolveToWebSymbols(): List<WebSymbol> =
asSequence()
.flatMap { it.resolveReference() }
.filterIsInstance<WebSymbol>()
.flatMap {
if (it is WebSymbolMatch
&& it.nameSegments.size == 1
&& it.nameSegments[0].canUnwrapSymbols()) {
it.nameSegments[0].symbols
}
else listOf(it)
}
.toList()
private fun PsiFile.webSymbolDeclarationsAt(offset: Int): Collection<WebSymbolDeclaration> {
for ((element, offsetInElement) in elementsAtOffsetUp(offset)) {
val declarations = WebSymbolDeclarationProvider.getAllDeclarations(element, offsetInElement)
if (declarations.isNotEmpty()) {
return declarations
}
}
return emptyList()
}
private fun <T> injectionThenHost(file: PsiFile, offset: Int, computation: (PsiFile, Int) -> T?): T? {
val injectedFile = InjectedLanguageManager.getInstance(file.project).findInjectedElementAt(file, offset)?.containingFile
if (injectedFile != null) {
PsiDocumentManager.getInstance(file.project).getDocument(injectedFile)
?.let { it as? DocumentWindow }
?.hostToInjected(offset)
?.takeIf { it >= 0 }
?.let { computation(injectedFile, it) }
?.let { return it }
}
return computation(file, offset)
}
fun CodeInsightTestFixture.resolveToWebSymbolSource(signature: String): PsiElement {
val webSymbol = resolveWebSymbolReference(signature)
val result = assertInstanceOf<PsiSourcedWebSymbol>(webSymbol).source
assertNotNull("WebSymbol $webSymbol source is null", result)
return result!!
}
fun CodeInsightTestFixture.resolveReference(signature: String): PsiElement {
val offsetBySignature = file.findOffsetBySignature(signature)
var ref = file.findReferenceAt(offsetBySignature)
if (ref === null) {
//possibly an injection
ref = InjectedLanguageManager.getInstance(project)
.findInjectedElementAt(file, offsetBySignature)
?.findReferenceAt(0)
}
assertNotNull("No reference at '$signature'", ref)
var resolve = ref!!.resolve()
if (resolve == null && ref is PsiPolyVariantReference) {
val results = ref.multiResolve(false).filter { it.isValidResult }
if (results.size > 1) {
throw AssertionError("Reference resolves to more than one element at '" + signature + "': "
+ results)
}
else if (results.size == 1) {
resolve = results[0].element
}
}
assertNotNull("Reference resolves to null at '$signature'", resolve)
return resolve!!
}
fun CodeInsightTestFixture.multiResolveReference(signature: String): List<PsiElement> {
val offsetBySignature = file.findOffsetBySignature(signature)
val ref = file.findReferenceAt(offsetBySignature)
assertNotNull("No reference at '$signature'", ref)
assertTrue("PsiPolyVariantReference expected", ref is PsiPolyVariantReference)
val resolveResult = (ref as PsiPolyVariantReference).multiResolve(false)
assertFalse("Empty reference resolution at '$signature'", resolveResult.isEmpty())
return resolveResult.mapNotNull { it.element }
}
@JvmOverloads
fun CodeInsightTestFixture.assertUnresolvedReference(signature: String, okWithNoRef: Boolean = false, allowSelfReference: Boolean = false) {
assertEmpty("Reference at $signature should not resolve to WebSymbols.", multiResolveWebSymbolReference(signature))
val offsetBySignature = file.findOffsetBySignature(signature)
val ref = file.findReferenceAt(offsetBySignature)
if (okWithNoRef && ref == null) {
return
}
assertNotNull("Expected not null reference for signature '$signature' at offset $offsetBySignature in file\n${file.text}", ref)
val resolved = ref!!.resolve()
if (ref.element == resolved && allowSelfReference) {
if (ref is PsiPolyVariantReference) {
assertEmpty(ref.multiResolve(false).filter { it.element != ref.element })
}
return
}
assertNull(
"Expected that reference for signature '$signature' at offset $offsetBySignature resolves to null but resolved to $resolved (${resolved?.text}) in file ${resolved?.containingFile?.name}",
resolved)
if (ref is PsiPolyVariantReference) {
assertEmpty(ref.multiResolve(false))
}
}
fun CodeInsightTestFixture.findUsages(target: SearchTarget): MutableCollection<out Usage> {
val project = project
val searchScope = coalesce<SearchScope>(target.maximalSearchScope, GlobalSearchScope.allScope(project))
val allOptions = AllSearchOptions(UsageOptions.createOptions(searchScope), true)
return buildUsageViewQuery(getProject(), target, allOptions).findAll()
}
@JvmOverloads
@RequiresEdt
fun CodeInsightTestFixture.checkGTDUOutcome(expectedOutcome: GotoDeclarationOrUsageHandler2.GTDUOutcome?, signature: String? = null) {
if (signature != null) {
moveToOffsetBySignature(signature)
}
val actualSignature = signature ?: editor.currentPositionSignature
val editor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, file)
val offset = editor.caretModel.offset
val gtduOutcome = ReadAction
.nonBlocking(Callable {
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: file
testGTDUOutcome(editor, file, offset)
})
.submit(AppExecutorUtil.getAppExecutorService())
.get()
Assert.assertEquals(actualSignature,
expectedOutcome,
gtduOutcome)
}
fun CodeInsightTestFixture.checkGotoDeclaration(fromSignature: String?, declarationSignature: String, expectedFileName: String? = null) {
checkGTDUOutcome(GotoDeclarationOrUsageHandler2.GTDUOutcome.GTD, fromSignature)
val actualSignature = fromSignature ?: editor.currentPositionSignature
performEditorAction("GotoDeclaration")
val targetEditor = FileEditorManager.getInstance(project).selectedTextEditor?.topLevelEditor
if (targetEditor == null) throw NullPointerException(actualSignature)
val targetFile = PsiDocumentManager.getInstance(project).getPsiFile(targetEditor.document)!!
if (expectedFileName != null) {
assertEquals(actualSignature, expectedFileName, PsiDocumentManager.getInstance(project).getPsiFile(targetEditor.document)?.name)
}
else {
assertEquals(actualSignature, targetEditor, editor.topLevelEditor)
}
if (!declarationSignature.contains("<caret>") || targetFile.findOffsetBySignature(
declarationSignature) != targetEditor.caretModel.offset) {
assertEquals("For go to from: $actualSignature",
declarationSignature + if (!declarationSignature.contains("<caret>")) ""
else (" [" + file.findOffsetBySignature(declarationSignature) + "]"),
targetEditor.currentPositionSignature + "[${targetEditor.caretModel.offset}]")
}
}
fun CodeInsightTestFixture.checkListByFile(actualList: List<String>, @TestDataFile expectedFile: String, containsCheck: Boolean) {
val path = "$testDataPath/$expectedFile"
val file = File(path)
if (!file.exists() && file.createNewFile()) {
Logger.getInstance("#WebTestUtilKt").warn("File $file has been created.")
}
val actualContents = actualList.joinToString("\n").trim() + "\n"
val expectedContents = FileUtil.loadFile(file, "UTF-8", true).trim() + "\n"
if (containsCheck) {
val expectedList = FileUtil.loadLines(file, "UTF-8").filter { it.isNotBlank() }
val actualSet = actualList.toSet()
if (!expectedList.all { actualSet.contains(it) }) {
throw FileComparisonFailedError(expectedFile, expectedContents, actualContents, path)
}
}
else if (expectedContents != actualContents) {
throw FileComparisonFailedError(expectedFile, expectedContents, actualContents, path)
}
}
fun CodeInsightTestFixture.checkTextByFile(actualContents: String, @TestDataFile expectedFile: String) {
val path = "$testDataPath/$expectedFile"
val file = File(path)
if (!file.exists() && file.createNewFile()) {
Logger.getInstance("#WebTestUtilKt").warn("File $file has been created.")
}
val actualContentsTrimmed = actualContents.trim() + "\n"
val expectedContents = FileUtil.loadFile(file, "UTF-8", true).trim() + "\n"
if (expectedContents != actualContentsTrimmed) {
throw FileComparisonFailedError(expectedFile, expectedContents, actualContentsTrimmed, path)
}
}
fun CodeInsightTestFixture.canRenameWebSymbolAtCaret() =
webSymbolAtCaret().let {
it is RenameTarget || it?.renameTarget != null || (it is PsiSourcedWebSymbol && it.source != null)
}
fun CodeInsightTestFixture.renameWebSymbol(newName: String) {
val symbol = webSymbolAtCaret() ?: throw AssertionError("No WebSymbol at caret")
var target: RenameTarget? = null
for (factory: SymbolRenameTargetFactory in SymbolRenameTargetFactory.EP_NAME.extensions) {
target = factory.renameTarget(project, symbol)
if (target != null) break
}
if (target == null) {
target = when (symbol) {
is RenameTarget -> symbol
is PsiSourcedWebSymbol -> {
val psiTarget = symbol.source
?: throw AssertionError("Symbol $symbol provides null source")
renameElement(psiTarget, newName)
return
}
else -> symbol.renameTarget
?: throw AssertionError("Symbol $symbol does not provide rename target nor is a PsiSourcedWebSymbol")
}
}
if (target.createPointer().dereference() == null) {
throw AssertionError("Target $target pointer dereferences to null")
}
renameTarget(target, newName)
}
fun doCompletionItemsTest(
fixture: CodeInsightTestFixture,
fileName: String,
goldFileWithExtension: Boolean = false,
renderDisplayText: Boolean = false,
) {
val fileNameNoExt = FileUtil.getNameWithoutExtension(fileName)
fixture.configureByFile(fileName)
WriteAction.runAndWait<Throwable> { WebSymbolsQueryExecutorFactory.getInstance(fixture.project) }
val document = fixture.getDocument(fixture.file)
val offsets = mutableListOf<Pair<Int, Boolean>>()
WriteAction.runAndWait<Throwable> {
CommandProcessor.getInstance().executeCommand(fixture.project, {
val chars = document.charsSequence
var pos: Int
while (chars.indexOf('|').also { pos = it } >= 0) {
val strict = chars.length > pos + 1 && chars[pos + 1] == '!'
offsets.add(Pair(pos, strict))
if (strict)
document.deleteString(pos, pos + 2)
else
document.deleteString(pos, pos + 1)
}
}, null, null)
PsiDocumentManager.getInstance(fixture.project).commitDocument(document)
}
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
noAutoComplete {
offsets.forEachIndexed { index, (offset, strict) ->
fixture.editor.caretModel.moveToOffset(offset)
fixture.completeBasic()
fixture.checkListByFile(
fixture.renderLookupItems(true, true, true, renderDisplayText = renderDisplayText),
"gold/${if (goldFileWithExtension) fileName else fileNameNoExt}.${index}.txt", !strict)
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
}
}
}
private val Editor.currentPositionSignature: String
get() {
val caretPos = caretModel.offset
val text = document.text
return (text.substring(max(0, caretPos - 15), caretPos) + "<caret>" +
text.substring(caretPos, min(caretPos + 15, text.length)))
.replace("\n", "\\n")
}
private val Editor.topLevelEditor
get() = if (this is EditorWindow) delegate else this

View File

@@ -1,128 +1,3 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.webSymbols.query
import com.intellij.util.applyIf
import com.intellij.webSymbols.*
import com.intellij.webSymbols.completion.WebSymbolCodeCompletionItem
import com.intellij.webSymbols.html.WebSymbolHtmlAttributeValue
import com.intellij.webSymbols.utils.completeMatch
import com.intellij.webSymbols.utils.nameSegments
import java.util.*
open class WebSymbolsDebugOutputPrinter : DebugOutputPrinter() {
private val parents = Stack<WebSymbol>()
override fun printValueImpl(builder: StringBuilder, level: Int, value: Any?): StringBuilder =
when (value) {
is WebSymbolCodeCompletionItem -> builder.printCodeCompletionItem(level, value)
is WebSymbol -> builder.printSymbol(level, value)
is WebSymbolHtmlAttributeValue -> builder.printAttributeValue(level, value)
is WebSymbolNameSegment -> builder.printSegment(level, value)
is WebSymbolApiStatus -> builder.printApiStatus(value)
is Set<*> -> builder.printSet(value)
else -> super.printValueImpl(builder, level, value)
}
override fun printRecursiveValue(builder: StringBuilder, level: Int, value: Any): StringBuilder =
if (value is WebSymbol)
if (parents.peek() == value) builder.append("<self>") else builder.append("<recursive>")
else
super.printRecursiveValue(builder, level, value)
private fun StringBuilder.printCodeCompletionItem(topLevel: Int, item: WebSymbolCodeCompletionItem): StringBuilder =
printObject(topLevel) { level ->
printProperty(level, "name", item.name)
printProperty(level, "priority", item.priority ?: WebSymbol.Priority.NORMAL)
printProperty(level, "proximity", item.proximity?.takeIf { it > 0 })
printProperty(level, "displayName", item.displayName.takeIf { it != item.name })
printProperty(level, "offset", item.offset.takeIf { it != 0 })
printProperty(level, "completeAfterInsert", item.completeAfterInsert.takeIf { it })
printProperty(level, "completeAfterChars", item.completeAfterChars.takeIf { it.isNotEmpty() })
printProperty(level, "aliases", item.aliases.takeIf { it.isNotEmpty() })
printProperty(level, "source", item.symbol)
}
private fun StringBuilder.printSet(set: Set<*>): StringBuilder {
return append(set.toString())
}
private fun StringBuilder.printSymbol(topLevel: Int, source: WebSymbol): StringBuilder {
if (parents.contains(source)) {
if (parents.peek() == source) append("<self>") else append("<recursive>")
return this
}
printObject(topLevel) { level ->
if (source.pattern != null) {
printProperty(level, "matchedName", source.namespace.lowercase(Locale.US) + "/" + source.kind + "/<pattern>")
printProperty(level, "name", source.name)
}
else {
printProperty(level, "matchedName", source.namespace.lowercase(Locale.US) + "/" + source.kind + "/" + source.name)
}
printProperty(level, "origin", "${source.origin.library}@${source.origin.version} (${source.origin.framework ?: "<none>"})")
printProperty(level, "source", (source as? PsiSourcedWebSymbol)?.source)
printProperty(level, "type", source.type)
printProperty(level, "attrValue", source.attributeValue)
printProperty(level, "complete", source.completeMatch)
printProperty(level, "description", source.description?.ellipsis(45))
printProperty(level, "docUrl", source.docUrl)
printProperty(level, "descriptionSections", source.descriptionSections.takeIf { it.isNotEmpty() })
printProperty(level, "abstract", source.abstract.takeIf { it })
printProperty(level, "virtual", source.virtual.takeIf { it })
printProperty(level, "apiStatus", source.apiStatus.takeIf { it !is WebSymbolApiStatus.Stable || it.since != null })
printProperty(level, "priority", source.priority ?: WebSymbol.Priority.NORMAL)
printProperty(level, "proximity", source.proximity?.takeIf { it > 0 })
printProperty(level, "has-pattern", if (source.pattern != null) true else null)
printProperty(level, "properties", source.properties.takeIf { it.isNotEmpty() })
parents.push(source)
printProperty(level, "segments", source.nameSegments)
parents.pop()
}
return this
}
private fun StringBuilder.printSegment(topLevel: Int,
segment: WebSymbolNameSegment): StringBuilder =
printObject(topLevel) { level ->
printProperty(level, "name-part", parents.peek().let { if (it.pattern == null) segment.getName(parents.peek()) else "" })
printProperty(level, "display-name", segment.displayName)
printProperty(level, "apiStatus", segment.apiStatus)
printProperty(level, "priority", segment.priority?.takeIf { it != WebSymbol.Priority.NORMAL })
printProperty(level, "matchScore", segment.matchScore.takeIf { it != segment.end - segment.start })
printProperty(level, "problem", segment.problem)
val symbols = segment.symbols.filter { !it.extension }
if (symbols.size > 1) {
printProperty(level, "symbols", symbols)
}
else if (symbols.size == 1) {
printProperty(level, "symbol", symbols[0])
}
}
private fun StringBuilder.printAttributeValue(topLevel: Int, value: WebSymbolHtmlAttributeValue): StringBuilder =
printObject(topLevel) { level ->
printProperty(level, "kind", value.kind)
.printProperty(level, "type", value.type)
.printProperty(level, "langType", value.langType)
.printProperty(level, "required", value.required)
.printProperty(level, "default", value.default)
}
private fun StringBuilder.printApiStatus(apiStatus: WebSymbolApiStatus): StringBuilder =
when (apiStatus) {
is WebSymbolApiStatus.Deprecated -> append("deprecated")
.applyIf(apiStatus.since != null) { append(" in ").append(apiStatus.since) }
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
is WebSymbolApiStatus.Obsolete -> append("obsolete")
.applyIf(apiStatus.since != null) { append(" in ").append(apiStatus.since) }
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
is WebSymbolApiStatus.Experimental -> append("experimental")
.applyIf(apiStatus.since != null) { append(" since ").append(apiStatus.since) }
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
is WebSymbolApiStatus.Stable -> append("stable")
.applyIf(apiStatus.since != null) { append(" since ").append(apiStatus.since) }
}
}