Skip to content

Commit 149d8a3

Browse files
BlaBlaHumanSpace Team
authored andcommitted
[Analysis API] Introduce AbstractAnalysisApiProjectPsiBasedTest for PSI-based project tests, rewrite AbstractKDocCoverageTest
1 parent 5c07fa0 commit 149d8a3

File tree

4 files changed

+209
-131
lines changed

4 files changed

+209
-131
lines changed

analysis/analysis-api/tests/org/jetbrains/kotlin/analysis/api/test/AnalysisApiKDocCoverageTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import org.jetbrains.kotlin.AbstractKDocCoverageTest
99
import org.junit.jupiter.api.Test
1010

1111
class AnalysisApiKDocCoverageTest : AbstractKDocCoverageTest() {
12-
override val sourceDirectories: List<DocumentationLocations> = listOf(
13-
DocumentationLocations(
12+
override val sourceDirectories: List<SourceDirectory.ForDumpFileComparison> = listOf(
13+
SourceDirectory.ForDumpFileComparison(
1414
listOf("analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api"),
1515
"analysis/analysis-api/api/analysis-api.undocumented",
1616
),
17-
DocumentationLocations(
17+
SourceDirectory.ForDumpFileComparison(
1818
listOf("analysis/analysis-api-platform-interface/src/org/jetbrains/kotlin/analysis/api/platform"),
1919
"analysis/analysis-api/api/analysis-api-platform-interface.undocumented",
2020
),
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
4+
*/
5+
6+
package org.jetbrains.kotlin
7+
8+
import com.intellij.openapi.vfs.StandardFileSystems
9+
import com.intellij.openapi.vfs.VirtualFileManager
10+
import com.intellij.openapi.vfs.VirtualFileSystem
11+
import com.intellij.psi.*
12+
import com.intellij.psi.util.childrenOfType
13+
import org.jetbrains.kotlin.AbstractAnalysisApiCodebaseTest.SourceDirectory
14+
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
15+
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
16+
import org.jetbrains.kotlin.cli.jvm.compiler.legacy.pipeline.createProjectEnvironment
17+
import org.jetbrains.kotlin.config.CompilerConfiguration
18+
import org.jetbrains.kotlin.psi.*
19+
import org.jetbrains.kotlin.psi.psiUtil.allChildren
20+
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
21+
import org.jetbrains.kotlin.test.KotlinTestUtils
22+
import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase
23+
import org.jetbrains.kotlin.test.util.KtTestUtil
24+
import java.io.File
25+
26+
/**
27+
* Test for checking various aspects of the Analysis API project itself based on PSI of files in the project.
28+
*
29+
* Traverses [sourceDirectories] and runs [traverse] on each of them.
30+
*
31+
* See [AbstractAnalysisApiCodebaseDumpFileComparisonTest] and [AbstractAnalysisApiCodebaseValidationTest]
32+
*/
33+
abstract class AbstractAnalysisApiCodebaseTest<T : SourceDirectory> : KtUsefulTestCase() {
34+
protected fun doTest() {
35+
val environment = createProjectEnvironment(
36+
CompilerConfiguration(),
37+
testRootDisposable,
38+
EnvironmentConfigFiles.JVM_CONFIG_FILES,
39+
MessageCollector.NONE
40+
)
41+
val psiManager = PsiManager.getInstance(environment.project)
42+
val fileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
43+
44+
sourceDirectories.forEach { sourceDirectory ->
45+
sourceDirectory.traverse(psiManager, fileSystem)
46+
}
47+
}
48+
49+
abstract fun T.traverse(psiManager: PsiManager, fileSystem: VirtualFileSystem)
50+
51+
abstract val sourceDirectories: List<T>
52+
53+
protected fun PsiElement.renderDeclaration(): String =
54+
getQualifiedName()?.let { fqn ->
55+
val parameterList = when (this) {
56+
is KtDeclaration -> getKtParameterList()
57+
else -> getParameterList()
58+
}
59+
fqn + parameterList
60+
} ?: "RENDERING ERROR"
61+
62+
protected fun SourceDirectory.getRoots(): List<File> {
63+
val homeDir = KtTestUtil.getHomeDirectory()
64+
return this.sourcePaths.map { File(homeDir, it) }
65+
}
66+
67+
protected fun File.getPsiFile(psiManager: PsiManager, fileSystem: VirtualFileSystem): PsiFile? {
68+
if (this.isDirectory) return null
69+
if (this.extension != "kt" && this.extension != "java") return null
70+
return createPsiFile(this.path, psiManager, fileSystem)
71+
}
72+
73+
sealed class SourceDirectory(val sourcePaths: List<String>) {
74+
class ForValidation(sourcePaths: List<String>) : SourceDirectory(sourcePaths)
75+
class ForDumpFileComparison(sourcePaths: List<String>, val outputFilePath: String) : SourceDirectory(sourcePaths)
76+
}
77+
78+
private fun PsiElement.getQualifiedName(): String? {
79+
return when (this) {
80+
is KtConstructor<*> -> this.containingClassOrObject?.getQualifiedName().let { classFqName ->
81+
"$classFqName:constructor"
82+
}
83+
is KtNamedDeclaration -> this.fqName?.asString()
84+
is PsiQualifiedNamedElement -> this.qualifiedName
85+
is PsiJvmMember -> this.containingClass?.qualifiedName?.let { classFqName ->
86+
val isConstructor = (this as? PsiMethod)?.isConstructor == true
87+
classFqName + ":" + if (isConstructor) "constructor" else (this.name ?: "")
88+
}
89+
else -> null
90+
}
91+
}
92+
93+
private fun PsiElement.getParameterList(): String {
94+
val parameterList =
95+
childrenOfType<PsiParameterList>().singleOrNull()
96+
?.allChildren
97+
?.filterIsInstance<PsiParameter>()?.joinToString(", ") {
98+
it.type.presentableText
99+
}?.let {
100+
"($it)"
101+
} ?: ""
102+
103+
return parameterList
104+
}
105+
106+
private fun KtDeclaration.getKtParameterList(): String {
107+
val parameterList =
108+
childrenOfType<KtParameterList>().singleOrNull()
109+
?.allChildren
110+
?.filterIsInstance<KtParameter>()?.joinToString(", ") {
111+
it.typeReference?.typeElement?.text ?: ""
112+
}?.let {
113+
"($it)"
114+
} ?: ""
115+
116+
return parameterList
117+
}
118+
119+
private fun createPsiFile(fileName: String, psiManager: PsiManager, fileSystem: VirtualFileSystem): PsiFile? {
120+
val file = fileSystem.findFileByPath(fileName) ?: error("File not found: $fileName")
121+
val psiFile = psiManager.findFile(file)
122+
return psiFile
123+
}
124+
}
125+
126+
/**
127+
* Test for checking the code base against the master file.
128+
*
129+
* Traverses [sourceDirectories] and triggers [processFile] on each contained file.
130+
* For each directory builds the resulting text out of lines returned from [processFile].
131+
* Then compares this text against the contents of the master file [SourceDirectory.ForDumpFileComparison.outputFilePath].
132+
*
133+
* If comparison fails, throws an exception with [getErrorMessage] as the message.
134+
*/
135+
abstract class AbstractAnalysisApiCodebaseDumpFileComparisonTest :
136+
AbstractAnalysisApiCodebaseTest<SourceDirectory.ForDumpFileComparison>() {
137+
override fun SourceDirectory.ForDumpFileComparison.traverse(psiManager: PsiManager, fileSystem: VirtualFileSystem) {
138+
val roots = getRoots()
139+
140+
val actualText = buildList {
141+
for (root in roots) {
142+
for (file in root.walkTopDown()) {
143+
val psiFile = file.getPsiFile(psiManager, fileSystem) ?: continue
144+
addAll(psiFile.processFile())
145+
}
146+
}
147+
}.sorted().joinToString("\n")
148+
149+
val expectedFile = getExpectedFile()
150+
val errorMessage = getErrorMessage()
151+
KotlinTestUtils.assertEqualsToFile(errorMessage, expectedFile, actualText)
152+
}
153+
154+
private fun SourceDirectory.ForDumpFileComparison.getExpectedFile(): File {
155+
val homeDir = KtTestUtil.getHomeDirectory()
156+
return File(homeDir, outputFilePath)
157+
}
158+
159+
abstract fun SourceDirectory.ForDumpFileComparison.getErrorMessage(): String
160+
abstract fun PsiFile.processFile(): List<String>
161+
}
162+
163+
164+
/**
165+
* Test for checking the code base without building the resulting text.
166+
*
167+
* Such a test can be used for properties that are not allowed to be violated.
168+
* The test is expected to directly throw any custom exceptions from [processFile] in case of violations.
169+
*/
170+
abstract class AbstractAnalysisApiCodebaseValidationTest :
171+
AbstractAnalysisApiCodebaseTest<SourceDirectory.ForValidation>() {
172+
override fun SourceDirectory.ForValidation.traverse(psiManager: PsiManager, fileSystem: VirtualFileSystem) {
173+
val roots = getRoots()
174+
175+
for (root in roots) {
176+
for (file in root.walkTopDown()) {
177+
val psiFile = file.getPsiFile(psiManager, fileSystem) ?: continue
178+
psiFile.processFile()
179+
}
180+
}
181+
}
182+
183+
abstract fun PsiFile.processFile()
184+
}

compiler/psi/psi-api/testFixtures/org/jetbrains/kotlin/AbstractKDocCoverageTest.kt

Lines changed: 20 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -6,149 +6,56 @@
66
package org.jetbrains.kotlin
77

88
import com.intellij.lang.jvm.JvmModifier
9-
import com.intellij.openapi.vfs.StandardFileSystems
10-
import com.intellij.openapi.vfs.VirtualFileManager
11-
import com.intellij.openapi.vfs.VirtualFileSystem
129
import com.intellij.psi.*
13-
import com.intellij.psi.util.childrenOfType
14-
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
15-
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
16-
import org.jetbrains.kotlin.config.CompilerConfiguration
1710
import org.jetbrains.kotlin.lexer.KtTokens
1811
import org.jetbrains.kotlin.name.FqName
1912
import org.jetbrains.kotlin.psi.*
20-
import org.jetbrains.kotlin.psi.psiUtil.allChildren
21-
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
2213
import org.jetbrains.kotlin.psi.psiUtil.isPublic
23-
import org.jetbrains.kotlin.test.KotlinTestUtils
24-
import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase
25-
import org.jetbrains.kotlin.test.util.KtTestUtil
26-
import java.io.File
2714

2815
/**
2916
* This test was introduced to automatically check that every public API from some module
3017
* is documented (i.e., has a KDoc attached).
3118
*
3219
* The test iterates through all the source directories [sourceDirectories] and
33-
* for each directory [DocumentationLocations.sourcePaths] builds a separate resulting file
20+
* for each directory [SourceDirectory.sourcePaths] builds a separate resulting file
3421
* containing all the undocumented public declarations along with fully qualified names and parameter types.
3522
*
3623
* Then the test compares the contents of the resulting file
37-
* and the master file [DocumentationLocations.outputFilePath]
24+
* and the master file [SourceDirectory.ForDumpFileComparison.outputFilePath]
3825
*
3926
* The test is intended to prevent developers from writing undocumented APIs.
4027
* If the lack of documentation for some declaration is intentional,
4128
* the developer has to manually add this declaration to the master file.
4229
*/
43-
abstract class AbstractKDocCoverageTest : KtUsefulTestCase() {
44-
@OptIn(K1Deprecation::class)
45-
protected fun doTest() {
46-
val environment = KotlinCoreEnvironment.createForParallelTests(
47-
testRootDisposable,
48-
CompilerConfiguration(),
49-
EnvironmentConfigFiles.JVM_CONFIG_FILES
50-
)
51-
val psiManager = PsiManager.getInstance(environment.project)
52-
val fileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
53-
val homeDir = KtTestUtil.getHomeDirectory()
54-
55-
sourceDirectories.forEach { (sourceCodeDirectoryPaths, outputFilePath) ->
56-
val roots = sourceCodeDirectoryPaths.map { File(homeDir, it) }
57-
58-
val actualText = buildList {
59-
for (root in roots) {
60-
for (file in root.walkTopDown()) {
61-
if (file.isDirectory) continue
62-
if (file.extension != "kt" && file.extension != "java") continue
63-
64-
val relativePath = file.relativeTo(root).invariantSeparatorsPath
65-
66-
try {
67-
val psiFile = createPsiFile(file.path, psiManager, fileSystem) ?: continue
68-
when (psiFile) {
69-
is KtFile if psiFile.packageFqName !in ignoredPackages -> addAll(
70-
getUndocumentedDeclarationsByFile(psiFile)
71-
)
72-
is PsiJavaFile if psiFile.packageName !in ignoredPackages.map { it.toString() } -> addAll(
73-
getUndocumentedDeclarationsByFile(psiFile)
74-
)
75-
}
76-
} catch (e: Exception) {
77-
throw IllegalStateException(relativePath, e)
78-
}
79-
}
80-
}
81-
}.sorted().joinToString("\n")
82-
83-
val expectedFile = File(homeDir, outputFilePath)
84-
val errorMessage = """
85-
The list of public undocumented declarations in `$roots` does not match the expected list in `$outputFilePath`.
86-
If you added new undocumented declarations, please document them or add them to the exclusion list.
87-
Otherwise, update the exclusion list accordingly.
88-
""".trimIndent()
89-
KotlinTestUtils.assertEqualsToFile(errorMessage, expectedFile, actualText)
30+
abstract class AbstractKDocCoverageTest : AbstractAnalysisApiCodebaseDumpFileComparisonTest() {
31+
override fun PsiFile.processFile(): List<String> = buildList {
32+
when (this@processFile) {
33+
is KtFile if packageFqName !in ignoredPackages -> addAll(
34+
getUndocumentedDeclarationsByFile(this@processFile)
35+
)
36+
is PsiJavaFile if packageName !in ignoredPackages.map { it.toString() } -> addAll(
37+
getUndocumentedDeclarationsByFile(this@processFile)
38+
)
9039
}
9140
}
9241

42+
override fun SourceDirectory.ForDumpFileComparison.getErrorMessage(): String =
43+
"""
44+
The list of public undocumented declarations in `${getRoots()}` does not match the expected list in `${outputFilePath}`.
45+
If you added new undocumented declarations, please document them.
46+
Otherwise, update the exclusion list accordingly.
47+
""".trimIndent()
48+
9349
private fun getUndocumentedDeclarationsByFile(file: KtFile): List<String> =
9450
file.collectPublicDeclarations()
9551
.filter { it.shouldBeRendered() }
96-
.map { renderDeclaration(it) }
52+
.map { it.renderDeclaration() }
9753

9854
private fun getUndocumentedDeclarationsByFile(file: PsiJavaFile): List<String> =
9955
file.collectPublicDeclarations()
10056
.filter { it.shouldBeRendered() }
101-
.map { renderDeclaration(it) }
102-
103-
private fun renderDeclaration(element: PsiElement): String =
104-
element.getQualifiedName()?.let { fqn ->
105-
val parameterList = when (element) {
106-
is KtDeclaration -> getKtParameterList(element)
107-
else -> getParameterList(element)
108-
}
109-
fqn + parameterList
110-
} ?: "RENDERING ERROR"
111-
112-
private fun PsiElement.getQualifiedName(): String? {
113-
return when (this) {
114-
is KtConstructor<*> -> this.containingClassOrObject?.getQualifiedName().let { classFqName ->
115-
"$classFqName:constructor"
116-
}
117-
is KtNamedDeclaration -> this.fqName?.asString()
118-
is PsiQualifiedNamedElement -> this.qualifiedName
119-
is PsiJvmMember -> this.containingClass?.qualifiedName?.let { classFqName ->
120-
val isConstructor = (this as? PsiMethod)?.isConstructor == true
121-
classFqName + ":" + if (isConstructor) "constructor" else (this.name ?: "")
122-
}
123-
else -> null
124-
}
125-
}
126-
127-
private fun getParameterList(element: PsiElement): String {
128-
val parameterList =
129-
element.childrenOfType<PsiParameterList>().singleOrNull()
130-
?.allChildren
131-
?.filterIsInstance<PsiParameter>()?.joinToString(", ") {
132-
it.type.presentableText
133-
}?.let {
134-
"($it)"
135-
} ?: ""
136-
137-
return parameterList
138-
}
57+
.map { it.renderDeclaration() }
13958

140-
private fun getKtParameterList(declaration: KtDeclaration): String {
141-
val parameterList =
142-
declaration.childrenOfType<KtParameterList>().singleOrNull()
143-
?.allChildren
144-
?.filterIsInstance<KtParameter>()?.joinToString(", ") {
145-
it.typeReference?.typeElement?.text ?: ""
146-
}?.let {
147-
"($it)"
148-
} ?: ""
149-
150-
return parameterList
151-
}
15259

15360
private fun KtFile.collectPublicDeclarations(): List<KtDeclaration> = buildList {
15461
this@collectPublicDeclarations.declarations.forEach { ktDeclaration ->
@@ -198,22 +105,9 @@ abstract class AbstractKDocCoverageTest : KtUsefulTestCase() {
198105
else -> (this as? PsiDocCommentOwner)?.docComment == null
199106
}
200107

201-
abstract val sourceDirectories: List<DocumentationLocations>
202-
203108
protected open val ignoredPropertyNames: List<String> = listOf()
204109

205110
protected open val ignoredFunctionNames: List<String> = listOf()
206111

207112
protected open val ignoredPackages: List<FqName> = listOf()
208-
209-
data class DocumentationLocations(
210-
val sourcePaths: List<String>,
211-
val outputFilePath: String,
212-
)
213-
214-
private fun createPsiFile(fileName: String, psiManager: PsiManager, fileSystem: VirtualFileSystem): PsiFile? {
215-
val file = fileSystem.findFileByPath(fileName) ?: error("File not found: $fileName")
216-
val psiFile = psiManager.findFile(file)
217-
return psiFile
218-
}
219113
}

0 commit comments

Comments
 (0)