From 79d7babb5761e70c7827e1f726332294447be7cd Mon Sep 17 00:00:00 2001 From: Pavel Kirpichenkov Date: Tue, 14 Jul 2020 12:52:22 +0300 Subject: [PATCH] Add test checking file annotations resolution #KT-37219 Fixed --- .../kotlin/generators/tests/GenerateTests.kt | 4 + .../resolveModeComparison/Classes.after | 21 +++ .../resolve/resolveModeComparison/Classes.kt | 21 +++ .../FileAnnotations.after | 14 ++ .../resolveModeComparison/FileAnnotations.kt | 14 ++ .../resolveModeComparison/Functions.after | 7 + .../resolveModeComparison/Functions.kt | 7 + .../NestedFunctions.after | 6 + .../resolveModeComparison/NestedFunctions.kt | 6 + .../AbstractResolveModeComparisonTest.kt | 166 ++++++++++++++++++ .../ResolveModeComparisonTestGenerated.java | 50 ++++++ 11 files changed, 316 insertions(+) create mode 100644 idea/testData/resolve/resolveModeComparison/Classes.after create mode 100644 idea/testData/resolve/resolveModeComparison/Classes.kt create mode 100644 idea/testData/resolve/resolveModeComparison/FileAnnotations.after create mode 100644 idea/testData/resolve/resolveModeComparison/FileAnnotations.kt create mode 100644 idea/testData/resolve/resolveModeComparison/Functions.after create mode 100644 idea/testData/resolve/resolveModeComparison/Functions.kt create mode 100644 idea/testData/resolve/resolveModeComparison/NestedFunctions.after create mode 100644 idea/testData/resolve/resolveModeComparison/NestedFunctions.kt create mode 100644 idea/tests/org/jetbrains/kotlin/idea/resolve/AbstractResolveModeComparisonTest.kt create mode 100644 idea/tests/org/jetbrains/kotlin/idea/resolve/ResolveModeComparisonTestGenerated.java diff --git a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index 20119225d86..282d973ee59 100644 --- a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -273,6 +273,10 @@ fun main(args: Array) { model("resolve/partialBodyResolve") } + testClass { + model("resolve/resolveModeComparison") + } + testClass { model("checker", recursive = false) model("checker/regression") diff --git a/idea/testData/resolve/resolveModeComparison/Classes.after b/idea/testData/resolve/resolveModeComparison/Classes.after new file mode 100644 index 00000000000..e1bb66c320c --- /dev/null +++ b/idea/testData/resolve/resolveModeComparison/Classes.after @@ -0,0 +1,21 @@ +/*All (1)*/class Foo/*None (2)*/(/*None (3)*/val prop: /*None (4)*/String/*(4)*//*(3)*/)/*(2)*/ { + /*None (5)*/class Inner { + /*None (6)*/val boo = 42/*(6)*/ + + /*None (7)*/fun inner() {}/*(7)*/ + }/*(5)*/ + + /*All (8)*/fun first(/*All (9)*/arg1: /*All (10)*/String/*(10)*//*(9)*/) { + /*All (11)*/arg1/*(11)*/ + }/*(8)*/ + + /*None (12)*/fun second(/*None (13)*/arg2: /*None (14)*/Int/*(14)*//*(13)*/) { + /*None (15)*/arg2/*(15)*/ + }/*(12)*/ +}/*(1)*/ + +/*None (16)*/class Bar/*None (17)*/(/*None (18)*/val baz: /*None (19)*/Int/*(19)*//*(18)*/)/*(17)*/ { + /*None (20)*/fun bee() { + /*None (21)*/val local = "abc"/*(21)*/ + }/*(20)*/ +}/*(16)*/ \ No newline at end of file diff --git a/idea/testData/resolve/resolveModeComparison/Classes.kt b/idea/testData/resolve/resolveModeComparison/Classes.kt new file mode 100644 index 00000000000..26b9a724948 --- /dev/null +++ b/idea/testData/resolve/resolveModeComparison/Classes.kt @@ -0,0 +1,21 @@ +class Foo(val prop: String) { + class Inner { + val boo = 42 + + fun inner() {} + } + + fun first(arg1: String) { + arg1 + } + + fun second(arg2: Int) { + arg2 + } +} + +class Bar(val baz: Int) { + fun bee() { + val local = "abc" + } +} \ No newline at end of file diff --git a/idea/testData/resolve/resolveModeComparison/FileAnnotations.after b/idea/testData/resolve/resolveModeComparison/FileAnnotations.after new file mode 100644 index 00000000000..154ef70b6a4 --- /dev/null +++ b/idea/testData/resolve/resolveModeComparison/FileAnnotations.after @@ -0,0 +1,14 @@ +// COMPILER_ARGUMENTS: -Xopt-in=kotlin.RequiresOptIn +@file:/*All (1)*/OptIn/*(1)*/(/*All (2)*/Experimental/*(2)*/::class) + +/*All (3)*/@/*All (4)*/Retention/*(4)*/(/*None (5)*/value/*(5)*/ = /*None (6)*/AnnotationRetention/*(6)*/./*None (7)*/BINARY/*(7)*/) +@/*All (8)*/RequiresOptIn/*(8)*/(/*All (9)*/level/*(9)*/ = /*All (10)*/RequiresOptIn/*(10)*/./*All (11)*/Level/*(11)*/./*All (12)*/WARNING/*(12)*/) +annotation class Experimental/*(3)*/ + +/*All (13)*/object MyObject/*(13)*/ + +/*All (14)*/@/*All (15)*/Experimental/*(15)*/ +operator fun /*All (16)*/MyObject/*(16)*/.invoke(/*All (17)*/closure: () -> /*All (18)*/Unit/*(18)*//*(17)*/) {}/*(14)*/ + +/*All (19)*/fun d() = /*All (20)*//*All (21)*/MyObject/*(21)*/ { +}/*(20)*//*(19)*/ \ No newline at end of file diff --git a/idea/testData/resolve/resolveModeComparison/FileAnnotations.kt b/idea/testData/resolve/resolveModeComparison/FileAnnotations.kt new file mode 100644 index 00000000000..29e40186507 --- /dev/null +++ b/idea/testData/resolve/resolveModeComparison/FileAnnotations.kt @@ -0,0 +1,14 @@ +// COMPILER_ARGUMENTS: -Xopt-in=kotlin.RequiresOptIn +@file:OptIn(Experimental::class) + +@Retention(value = AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +annotation class Experimental + +object MyObject + +@Experimental +operator fun MyObject.invoke(closure: () -> Unit) {} + +fun d() = MyObject { +} \ No newline at end of file diff --git a/idea/testData/resolve/resolveModeComparison/Functions.after b/idea/testData/resolve/resolveModeComparison/Functions.after new file mode 100644 index 00000000000..7dab77719cf --- /dev/null +++ b/idea/testData/resolve/resolveModeComparison/Functions.after @@ -0,0 +1,7 @@ +/*All (1)*/fun foo(/*All (2)*/arg: /*All (3)*/String/*(3)*//*(2)*/) { + /*All (4)*/arg/*(4)*/ +}/*(1)*/ + +/*None (5)*/fun bar(/*None (6)*/arg: /*None (7)*/Int/*(7)*//*(6)*/) { + /*None (8)*/arg/*(8)*/ +}/*(5)*/ \ No newline at end of file diff --git a/idea/testData/resolve/resolveModeComparison/Functions.kt b/idea/testData/resolve/resolveModeComparison/Functions.kt new file mode 100644 index 00000000000..52d5d42049a --- /dev/null +++ b/idea/testData/resolve/resolveModeComparison/Functions.kt @@ -0,0 +1,7 @@ +fun foo(arg: String) { + arg +} + +fun bar(arg: Int) { + arg +} \ No newline at end of file diff --git a/idea/testData/resolve/resolveModeComparison/NestedFunctions.after b/idea/testData/resolve/resolveModeComparison/NestedFunctions.after new file mode 100644 index 00000000000..9f046c354f5 --- /dev/null +++ b/idea/testData/resolve/resolveModeComparison/NestedFunctions.after @@ -0,0 +1,6 @@ +/*All (1)*/fun test(/*All (2)*/baz: /*All (3)*/Int/*(3)*//*(2)*/) { + /*Full (4)*/fun irrelevant() { + /*Full (5)*/val foo = "bar"/*(5)*/ + }/*(4)*/ + /*All (6)*/baz/*(6)*/ /*None (7)*/=/*(7)*/ 42 +}/*(1)*/ \ No newline at end of file diff --git a/idea/testData/resolve/resolveModeComparison/NestedFunctions.kt b/idea/testData/resolve/resolveModeComparison/NestedFunctions.kt new file mode 100644 index 00000000000..d8c8225dc99 --- /dev/null +++ b/idea/testData/resolve/resolveModeComparison/NestedFunctions.kt @@ -0,0 +1,6 @@ +fun test(baz: Int) { + fun irrelevant() { + val foo = "bar" + } + baz = 42 +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/resolve/AbstractResolveModeComparisonTest.kt b/idea/tests/org/jetbrains/kotlin/idea/resolve/AbstractResolveModeComparisonTest.kt new file mode 100644 index 00000000000..59632984ca1 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/resolve/AbstractResolveModeComparisonTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.resolve + +import com.intellij.openapi.util.io.FileUtil +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.containers.MultiMap +import org.jetbrains.kotlin.backend.common.pop +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase +import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor +import org.jetbrains.kotlin.idea.test.withCustomCompilerOptions +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.test.KotlinTestUtils +import java.io.File + +/** + * This test runs analysis, then for each declaration and reference writes modes in which they have been resolved, if any. + * "Targeted" resolve for selected expression is used for analysis. + */ +abstract class AbstractResolveModeComparisonTest : KotlinLightCodeInsightFixtureTestCase() { + override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE + + fun doTest(unused: String) { + val testPath = testPath() + val dumpResults = dump(testPath) + + val testPathNoExt = FileUtil.getNameWithoutExtension(testPath) + KotlinTestUtils.assertEqualsToFile(File("$testPathNoExt.after"), dumpResults) + } + + private fun dump(testPath: String): String { + myFixture.configureByText(KotlinFileType.INSTANCE, File(testPath).readText()) + return withCustomCompilerOptions(myFixture.file.text, project, module) { + val file = myFixture.file as KtFile + val resolutionFacade = file.getResolutionFacade() + + val expression = findTargetExpression() + val analysisResults: Map = + RESOLVE_MODES.associateWith { resolveMode -> resolutionFacade.analyze(expression, resolveMode) } + + val markup: Map> = markResolveModes(analysisResults) + val offsets: MultiMap = findOffsets(markup) + + composeTextWithMarkup(offsets) + } + } + + private fun findTargetExpression(): KtExpression { + val editor = myFixture.editor + val selectionModel = editor.selectionModel + + return if (selectionModel.hasSelection()) { + PsiTreeUtil.findElementOfClassAtRange( + file, + selectionModel.selectionStart, + selectionModel.selectionEnd, + KtExpression::class.java + ) + ?: error("No KtExpression at selection range") + } else { + val offset = editor.caretModel.offset + val element = file.findElementAt(offset)!! + element.getNonStrictParentOfType() ?: error("No KtExpression at caret") + } + } + + private fun markResolveModes(analysisResults: Map): Map> { + val modesPerformingResolveForElements = hashMapOf>() + file.accept(object : KtTreeVisitorVoid() { + override fun visitReferenceExpression(reference: KtReferenceExpression) { + super.visitReferenceExpression(reference) + modesPerformingResolveForElements[reference] = analysisResults.keys.filter { bodyResolveMode -> + val bindingContext = analysisResults[bodyResolveMode] ?: error("No resolution results for mode: $bodyResolveMode") + bindingContext[BindingContext.REFERENCE_TARGET, reference] != null + } + } + + override fun visitDeclaration(declaration: KtDeclaration) { + super.visitDeclaration(declaration) + modesPerformingResolveForElements[declaration] = analysisResults.keys.filter { bodyResolveMode -> + val bindingContext = analysisResults[bodyResolveMode] ?: error("No resolution results for mode: $bodyResolveMode") + bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration] != null + } + } + }) + + return modesPerformingResolveForElements + } + + private fun findOffsets(resolveModesMarkup: Map>): MultiMap { + val offsets = MultiMap.createSmart() + for ((ktElement, resolveModesForElement) in resolveModesMarkup) { + val start = ktElement.startOffset + val end = ktElement.endOffset + if (resolveModesForElement.isEmpty()) + offsets.putValue(start, Mark.NotAnalyzed()) + else + offsets.putValue(start, Mark.Analyzed(resolveModesForElement)) + offsets.putValue(end, Mark.End()) + } + return offsets + } + + private fun composeTextWithMarkup(offsets: MultiMap): String { + val builder = StringBuilder() + var offsetBefore = 0 + var counter = 0 + val endIndices = ArrayDeque() + for ((offset, marks) in offsets.entrySet().sortedBy { it.key }) { + builder.append(file.text.substring(offsetBefore, offset)) + for (mark in marks) { + if (mark !is Mark.End) { + counter += 1 + endIndices.add(counter) + } + + when (mark) { + is Mark.Analyzed -> { + val modes = mark.modes!! + val modesRepresentation = + if (modes.size == RESOLVE_MODES.size) "All" + else modes.joinToString { it.toString().toLowerCase().capitalize() } + builder.append("/*${modesRepresentation} ($counter)*/") + } + is Mark.NotAnalyzed -> { + builder.append("/*None ($counter)*/") + } + is Mark.End -> { + builder.append("/*(${endIndices.pop()})*/") + } + else -> error("Sealed") + } + } + offsetBefore = offset + } + builder.append(file.text.substring(offsetBefore, file.textLength)) + + return builder.toString() + } + + private sealed class Mark(val modes: List? = null) { + class NotAnalyzed : Mark() + class Analyzed(modes: List) : Mark(modes) { + init { + require(modes.isNotEmpty()) { "'Analyzed' mark should not be created without modes" } + } + } + + class End : Mark() + } + + private companion object { + val RESOLVE_MODES: List = listOf( + BodyResolveMode.PARTIAL, + BodyResolveMode.FULL, // Note that due to caching it's important to do the full resolve as the last one + ) + } +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/resolve/ResolveModeComparisonTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/resolve/ResolveModeComparisonTestGenerated.java new file mode 100644 index 00000000000..ebee6138f84 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/resolve/ResolveModeComparisonTestGenerated.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.resolve; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/testData/resolve/resolveModeComparison") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class ResolveModeComparisonTestGenerated extends AbstractResolveModeComparisonTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + public void testAllFilesPresentInResolveModeComparison() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/resolve/resolveModeComparison"), Pattern.compile("^(.+)\\.kt$"), null, true); + } + + @TestMetadata("Classes.kt") + public void testClasses() throws Exception { + runTest("idea/testData/resolve/resolveModeComparison/Classes.kt"); + } + + @TestMetadata("FileAnnotations.kt") + public void testFileAnnotations() throws Exception { + runTest("idea/testData/resolve/resolveModeComparison/FileAnnotations.kt"); + } + + @TestMetadata("Functions.kt") + public void testFunctions() throws Exception { + runTest("idea/testData/resolve/resolveModeComparison/Functions.kt"); + } + + @TestMetadata("NestedFunctions.kt") + public void testNestedFunctions() throws Exception { + runTest("idea/testData/resolve/resolveModeComparison/NestedFunctions.kt"); + } +}