mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 15:09:39 +07:00
IJPL-189337 Moved ApiCheckTest to community
GitOrigin-RevId: e798bed7257f80b5f647b098b6673bc7c2c3cec8
This commit is contained in:
committed by
intellij-monorepo-bot
parent
b308cc54ce
commit
d16e748a66
1
.idea/modules.xml
generated
1
.idea/modules.xml
generated
@@ -943,6 +943,7 @@
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/junit5/highlighting/intellij.platform.testFramework.junit5.highlighting.iml" filepath="$PROJECT_DIR$/platform/testFramework/junit5/highlighting/intellij.platform.testFramework.junit5.highlighting.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/junit5.jimfs/intellij.platform.testFramework.junit5.jimfs.iml" filepath="$PROJECT_DIR$/platform/testFramework/junit5.jimfs/intellij.platform.testFramework.junit5.jimfs.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/junit5/projectStructure/intellij.platform.testFramework.junit5.projectStructure.iml" filepath="$PROJECT_DIR$/platform/testFramework/junit5/projectStructure/intellij.platform.testFramework.junit5.projectStructure.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/monorepo/intellij.platform.testFramework.monorepo.iml" filepath="$PROJECT_DIR$/platform/testFramework/monorepo/intellij.platform.testFramework.monorepo.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/teamCity/intellij.platform.testFramework.teamCity.iml" filepath="$PROJECT_DIR$/platform/testFramework/teamCity/intellij.platform.testFramework.teamCity.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/ui/intellij.platform.testFramework.ui.iml" filepath="$PROJECT_DIR$/platform/testFramework/ui/intellij.platform.testFramework.ui.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/testRunner/intellij.platform.testRunner.iml" filepath="$PROJECT_DIR$/platform/testRunner/intellij.platform.testRunner.iml" />
|
||||
|
||||
13
.idea/runConfigurations/ApiCheckTest.xml
generated
Normal file
13
.idea/runConfigurations/ApiCheckTest.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="ApiCheckTest" type="JUnit" factoryName="JUnit" nameIsGenerated="true">
|
||||
<module name="intellij.platform.testFramework.monorepo" />
|
||||
<shortenClasspath name="ARGS_FILE" />
|
||||
<option name="PACKAGE_NAME" value="com.intellij.platform.testFramework.monorepo.api" />
|
||||
<option name="MAIN_CLASS_NAME" value="com.intellij.platform.testFramework.monorepo.api.ApiCheckTest" />
|
||||
<option name="METHOD_NAME" value="" />
|
||||
<option name="TEST_OBJECT" value="class" />
|
||||
<method v="2">
|
||||
<option name="MakeProject" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -251,5 +251,6 @@
|
||||
<orderEntry type="module" module-name="intellij.vcs.git.commit.modal" scope="RUNTIME" />
|
||||
<orderEntry type="module" module-name="intellij.python.pyproject" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.indexing.tests" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.monorepo" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
41
platform/testFramework/monorepo/BUILD.bazel
Normal file
41
platform/testFramework/monorepo/BUILD.bazel
Normal file
@@ -0,0 +1,41 @@
|
||||
### auto-generated section `build intellij.platform.testFramework.monorepo` start
|
||||
load("@rules_java//java:defs.bzl", "java_library")
|
||||
load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_test")
|
||||
|
||||
java_library(
|
||||
name = "monorepo",
|
||||
visibility = ["//visibility:public"]
|
||||
)
|
||||
|
||||
jvm_library(
|
||||
name = "monorepo_test_lib",
|
||||
visibility = ["//visibility:public"],
|
||||
srcs = glob(["tests/**/*.kt", "tests/**/*.java"], allow_empty = True),
|
||||
deps = [
|
||||
"@lib//:junit5Jupiter",
|
||||
"//platform/testFramework/junit5",
|
||||
"//platform/testFramework/junit5:junit5_test_lib",
|
||||
"@lib//:kotlin-stdlib",
|
||||
"//platform/util/coroutines",
|
||||
"@lib//:kotlinx-coroutines-core",
|
||||
"@lib//:kotlinx-coroutines-test",
|
||||
"@lib//:jetbrains-annotations",
|
||||
"//jps/model-api:model",
|
||||
"//tools/apiDump",
|
||||
"//platform/util/diff",
|
||||
"//platform/util-ex",
|
||||
"@lib//:junit5",
|
||||
"//platform/util",
|
||||
"//platform/testFramework/core",
|
||||
"//jps/jps-builders:build",
|
||||
"//jps/model-serialization",
|
||||
"//platform/testFramework",
|
||||
"//platform/testFramework:testFramework_test_lib",
|
||||
]
|
||||
)
|
||||
|
||||
jvm_test(
|
||||
name = "monorepo_test",
|
||||
runtime_deps = [":monorepo_test_lib"]
|
||||
)
|
||||
### auto-generated section `build intellij.platform.testFramework.monorepo` end
|
||||
71
platform/testFramework/monorepo/exposed-third-party-api.txt
Normal file
71
platform/testFramework/monorepo/exposed-third-party-api.txt
Normal file
@@ -0,0 +1,71 @@
|
||||
java/awt/*
|
||||
java/awt/color/ColorSpace
|
||||
java/awt/datatransfer/*
|
||||
java/awt/dnd/DragSourceDropEvent
|
||||
java/awt/dnd/DropTargetListener
|
||||
java/awt/event/*
|
||||
java/awt/font/*
|
||||
java/awt/geom/*
|
||||
java/awt/im/InputMethodRequests
|
||||
java/awt/image/*
|
||||
java/awt/image/renderable/RenderableImage
|
||||
java/beans/PropertyChangeEvent
|
||||
java/beans/PropertyChangeListener
|
||||
java/beans/PropertyChangeSupport
|
||||
java/io/*
|
||||
java/lang/*
|
||||
java/lang/annotation/*
|
||||
java/lang/invoke/MethodHandle
|
||||
java/lang/ref/*
|
||||
java/lang/reflect/*
|
||||
java/math/BigInteger
|
||||
java/net/*
|
||||
java/net/http/*
|
||||
java/nio/**
|
||||
java/rmi/Remote
|
||||
java/rmi/server/Unreferenced
|
||||
java/security/*
|
||||
java/security/cert/*
|
||||
java/text/*
|
||||
java/time/*
|
||||
java/util/*
|
||||
java/util/concurrent/*
|
||||
java/util/concurrent/atomic/*
|
||||
java/util/concurrent/locks/*
|
||||
java/util/function/*
|
||||
java/util/jar/Attributes$Name
|
||||
java/util/jar/Manifest
|
||||
java/util/logging/*
|
||||
java/util/regex/Pattern
|
||||
java/util/stream/*
|
||||
java/util/zip/*
|
||||
javax/accessibility/*
|
||||
javax/net/ssl/*
|
||||
javax/swing/*
|
||||
javax/swing/border/*
|
||||
javax/swing/event/*
|
||||
javax/swing/plaf/*
|
||||
javax/swing/plaf/basic/*
|
||||
javax/swing/plaf/metal/*
|
||||
javax/swing/table/*
|
||||
javax/swing/text/*
|
||||
javax/swing/text/html/*
|
||||
javax/swing/tree/*
|
||||
javax/swing/undo/UndoManager
|
||||
kotlin/Lazy
|
||||
kotlin/Pair
|
||||
kotlin/Unit
|
||||
kotlin/coroutines/*
|
||||
kotlin/enums/EnumEntries
|
||||
kotlin/jvm/functions/*
|
||||
kotlin/jvm/internal/markers/KMappedMarker
|
||||
kotlin/properties/ObservableProperty
|
||||
kotlin/properties/ReadOnlyProperty
|
||||
kotlin/properties/ReadWriteProperty
|
||||
kotlin/ranges/ClosedRange
|
||||
kotlin/ranges/IntRange
|
||||
kotlin/sequences/Sequence
|
||||
kotlinx/coroutines/*
|
||||
kotlinx/coroutines/channels/*
|
||||
kotlinx/coroutines/flow/*
|
||||
kotlinx/serialization/KSerializer
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="JUnit5Jupiter" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="kotlin-stdlib" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.coroutines" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="kotlinx-coroutines-core" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="kotlinx-coroutines-test" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="jetbrains-annotations" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jps.model" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.tools.apiDump" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.diff" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.ex" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.core" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jps.build" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jps.model.serialization" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.platform.testFramework.monorepo
|
||||
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.project.IntelliJProjectConfiguration
|
||||
import com.intellij.testFramework.PlatformTestUtil
|
||||
import org.jetbrains.jps.model.JpsProject
|
||||
import org.jetbrains.jps.model.java.JavaSourceRootType
|
||||
import org.jetbrains.jps.model.library.JpsLibrary
|
||||
import org.jetbrains.jps.model.library.JpsOrderRootType
|
||||
import org.jetbrains.jps.model.module.JpsModule
|
||||
import org.jetbrains.jps.model.serialization.JpsModelSerializationDataService
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
|
||||
object MonorepoProjectStructure {
|
||||
val communityHomePath: String = PlatformTestUtil.getCommunityPath()
|
||||
val communityRoot: Path = Path(communityHomePath)
|
||||
val communityProject: JpsProject by lazy { IntelliJProjectConfiguration.loadIntelliJProject(communityHomePath) }
|
||||
|
||||
val JpsModule.baseDirectory: File
|
||||
get() = JpsModelSerializationDataService.getModuleExtension(this)!!.baseDirectory
|
||||
fun JpsModule.isFromCommunity(): Boolean = FileUtil.isAncestor(File(communityHomePath), this.baseDirectory, false)
|
||||
fun JpsLibrary.isFromCommunity(): Boolean = getFiles(JpsOrderRootType.COMPILED).all {
|
||||
FileUtil.isAncestor(File(communityHomePath), it, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun JpsModule.hasProductionSources(): Boolean = getSourceRoots(JavaSourceRootType.SOURCE).iterator().hasNext()
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.intellij.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.platform.testFramework.monorepo.MonorepoProjectStructure
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
class ApiCheckTest {
|
||||
|
||||
companion object {
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private val cs: CoroutineScope = GlobalScope.childScope("ApiCheckTest")
|
||||
|
||||
@AfterAll
|
||||
@JvmStatic
|
||||
fun afterAll() {
|
||||
cs.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
fun api(): List<DynamicTest> = performApiCheckTest(cs, MonorepoProjectStructure.communityProject.modules)
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// 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.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.platform.testFramework.core.FileComparisonFailedError
|
||||
import com.intellij.tools.apiDump.*
|
||||
import com.intellij.util.diff.Diff
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.jps.model.module.JpsModule
|
||||
import org.jetbrains.jps.util.JpsPathUtil
|
||||
import org.junit.jupiter.api.*
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.iterator
|
||||
import kotlin.collections.toTypedArray
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
|
||||
fun performApiCheckTest(cs: CoroutineScope, modules: List<JpsModule>): List<DynamicTest> = buildList {
|
||||
val exposedThirdPartyApiFilter: FileApiClassFilter = globalExposedThirdPartyClasses(modules)
|
||||
val moduleApi = ModuleApi(cs)
|
||||
for (module in modules) {
|
||||
val contentRootPath = module.firstContentRoot() ?: continue
|
||||
val stableApiDumpPath = contentRootPath.stableApiDumpPath()
|
||||
val unreviewedApiDumpPath = contentRootPath.unreviewedApiDumpPath()
|
||||
if (!stableApiDumpPath.exists() && !unreviewedApiDumpPath.exists()) {
|
||||
continue
|
||||
}
|
||||
val experimentalApiDumpPath = contentRootPath.experimentalApiDumpPath() // may not exist
|
||||
moduleApi.discoverModule(module)
|
||||
this += DynamicTest.dynamicTest(module.getTestName()) {
|
||||
val moduleName = module.name
|
||||
val api = runBlocking {
|
||||
moduleApi.moduleApi(module)
|
||||
}
|
||||
val checks = ArrayList<() -> Unit>(7)
|
||||
checks.addAll(checkModuleDump(moduleName, stableApiDumpPath, unreviewedApiDumpPath, api.stableApi))
|
||||
checks.addAll(checkModuleDump(moduleName, experimentalApiDumpPath, null, api.experimentalApi))
|
||||
|
||||
val privateApiFilter = (contentRootPath / exposedPrivateApiFileName).apiClassFilter()
|
||||
val thirdPartyApiFilter = (contentRootPath / exposedThirdPartyApiFileName).apiClassFilter()
|
||||
|
||||
val apiFilter: ApiClassFilter = exposedThirdPartyApiFilter
|
||||
.andThen(privateApiFilter ?: ApiClassFilter.Empty)
|
||||
.andThen(thirdPartyApiFilter ?: ApiClassFilter.Empty)
|
||||
checks.add {
|
||||
checkExposedApi(moduleName, api, apiFilter)
|
||||
}
|
||||
|
||||
fun checkFilter(filter: FileApiClassFilter?) {
|
||||
if (filter == null) {
|
||||
return
|
||||
}
|
||||
checks.add {
|
||||
filter.checkAllEntriesAreUsed()
|
||||
}
|
||||
checks.add {
|
||||
filter.checkEntriesSortedAndUnique()
|
||||
}
|
||||
}
|
||||
checkFilter(privateApiFilter)
|
||||
checkFilter(thirdPartyApiFilter)
|
||||
|
||||
assertAll(checks)
|
||||
}
|
||||
}
|
||||
this += DynamicTest.dynamicTest(exposedThirdPartyApiFileName) {
|
||||
assertAll(
|
||||
{
|
||||
exposedThirdPartyApiFilter.checkAllEntriesAreUsed()
|
||||
},
|
||||
{
|
||||
exposedThirdPartyApiFilter.checkEntriesSortedAndUnique()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun JpsModule.getTestName(): String =
|
||||
// without this, TC treats the part after the last dot as a test name
|
||||
name.replace(".", "-")
|
||||
|
||||
internal fun globalExposedThirdPartyClasses(modules: List<JpsModule>): FileApiClassFilter =
|
||||
modules
|
||||
.first { it.name == "intellij.platform.testFramework.monorepo" }
|
||||
.contentRootsList.urls
|
||||
.firstNotNullOf { Path.of(JpsPathUtil.urlToPath(it), exposedThirdPartyApiFileName) }
|
||||
.apiClassFilter()!!
|
||||
|
||||
const val MODULE_API_DUMP_FILE_NAME = "api-dump.txt"
|
||||
const val MODULE_API_DUMP_EXPERIMENTAL_FILE_NAME = "api-dump-experimental.txt"
|
||||
const val MODULE_API_DUMP_UNREVIEWED_FILE_NAME = "api-dump-unreviewed.txt"
|
||||
|
||||
internal fun JpsModule.firstContentRoot(): Path? {
|
||||
val contentRoot = contentRootsList.urls.firstOrNull()
|
||||
?: return null
|
||||
return Path.of(JpsPathUtil.urlToPath(contentRoot))
|
||||
}
|
||||
|
||||
internal fun Path.stableApiDumpPath(): Path = this / MODULE_API_DUMP_FILE_NAME
|
||||
|
||||
internal fun Path.unreviewedApiDumpPath(): Path = this / MODULE_API_DUMP_UNREVIEWED_FILE_NAME
|
||||
|
||||
internal fun Path.experimentalApiDumpPath(): Path {
|
||||
return this / MODULE_API_DUMP_EXPERIMENTAL_FILE_NAME
|
||||
}
|
||||
|
||||
private data class ExpectedDump(
|
||||
val path: Path,
|
||||
val rawContent: String,
|
||||
val pathExists: Boolean,
|
||||
val categorizedDump: Map<ClassName, ClassMembers>,
|
||||
val simplifiedBuilder: StringBuilder = StringBuilder(),
|
||||
) {
|
||||
companion object {
|
||||
fun build(path: Path): ExpectedDump {
|
||||
val exists = path.exists()
|
||||
val content = if (exists) path.readText().replace("\r\n", "\n") else ""
|
||||
val lines = Diff.splitLines(content)
|
||||
return ExpectedDump(path, content, exists, lines.groupByClasses())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkModuleDump(moduleName: String, primaryExpectedDumpPath: Path, secondaryExpectedDumpPath: Path?, classSignatures: List<ApiClass>): List<() -> Unit> {
|
||||
val primaryExpectedDump = ExpectedDump.build(primaryExpectedDumpPath)
|
||||
val secondaryExpectedDump = secondaryExpectedDumpPath?.let { ExpectedDump.build(it) }
|
||||
val actualCategorizedDump = dumpApiAndGroupByClasses(classSignatures)
|
||||
// partition of the actual dump that corresponds to the primary dump
|
||||
val primaryActualBuilder = StringBuilder()
|
||||
// partition of the actual dump that corresponds to the secondary dump
|
||||
val secondaryActualBuilder = StringBuilder()
|
||||
val mutablePrimaryDump = primaryExpectedDump.categorizedDump.toMutableMap()
|
||||
val mutableSecondaryDump = secondaryExpectedDump?.categorizedDump?.toMutableMap()
|
||||
for ((className, actualMembers) in actualCategorizedDump) {
|
||||
val primaryMembers = mutablePrimaryDump.remove(className)
|
||||
val secondaryMembers = mutableSecondaryDump?.remove(className)
|
||||
|
||||
if (primaryMembers == null && secondaryMembers == null) {
|
||||
// new classes go to the primary dump by default
|
||||
primaryActualBuilder.appendLine(className)
|
||||
primaryExpectedDump.simplifiedBuilder.appendLine("+ $className")
|
||||
actualMembers.forEach {
|
||||
primaryActualBuilder.appendLine(it)
|
||||
primaryExpectedDump.simplifiedBuilder.appendLine("+ $it")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// members that are physically written in `api-dump*.txt`
|
||||
val serializedPrimaryMembers = primaryMembers?.toMutableSet()
|
||||
val serializedSecondaryMembers = secondaryMembers?.toMutableSet()
|
||||
|
||||
// containers for actual API members
|
||||
val actualPrimaryMembers = mutableListOf<String>()
|
||||
val actualSecondaryMembers = mutableListOf<String>()
|
||||
|
||||
var headerAdded = false
|
||||
|
||||
for (actualMember in actualMembers) {
|
||||
if (serializedPrimaryMembers?.contains(actualMember) == true) {
|
||||
// member is mentioned in the primary dump; skipping it
|
||||
serializedPrimaryMembers.remove(actualMember)
|
||||
actualPrimaryMembers.add(actualMember)
|
||||
}
|
||||
else if (serializedSecondaryMembers?.contains(actualMember) == true) {
|
||||
// member is mentioned in the secondary dump; skipping it
|
||||
serializedSecondaryMembers.remove(actualMember)
|
||||
actualSecondaryMembers.add(actualMember)
|
||||
}
|
||||
else {
|
||||
// member is not mentioned anywhere; adding it to the primary dump by default
|
||||
if (!headerAdded) {
|
||||
if (primaryMembers == null) {
|
||||
// the class was not previously mentioned in the primary dump, let's add class name
|
||||
primaryExpectedDump.simplifiedBuilder.appendLine("+ $className")
|
||||
}
|
||||
headerAdded = true
|
||||
}
|
||||
primaryExpectedDump.simplifiedBuilder.appendLine("+ $actualMember")
|
||||
actualPrimaryMembers.add(actualMember)
|
||||
}
|
||||
}
|
||||
|
||||
// building partition of actual API dump by primary and secondary dumps
|
||||
appendMembersToActualDump(primaryActualBuilder, actualPrimaryMembers, className, primaryMembers != null)
|
||||
appendMembersToActualDump(secondaryActualBuilder, actualSecondaryMembers, className, secondaryMembers != null)
|
||||
|
||||
// suggesting to remove leftover members from the primary
|
||||
removeLeftoverMethods(serializedPrimaryMembers, primaryExpectedDump.simplifiedBuilder)
|
||||
if (serializedSecondaryMembers?.isNotEmpty() == true && serializedSecondaryMembers.size == secondaryMembers.size) {
|
||||
// we moved all members from secondary dump; let's remove the class name altogether
|
||||
secondaryExpectedDump.simplifiedBuilder.appendLine("- $className")
|
||||
}
|
||||
removeLeftoverMethods(serializedSecondaryMembers, secondaryExpectedDump?.simplifiedBuilder)
|
||||
}
|
||||
|
||||
cleanupClassesThatDoNotExist(mutablePrimaryDump, primaryExpectedDump)
|
||||
if (mutableSecondaryDump != null) {
|
||||
cleanupClassesThatDoNotExist(mutableSecondaryDump, secondaryExpectedDump)
|
||||
}
|
||||
|
||||
fun runCheck(expectedDump: ExpectedDump, actualDump: String) {
|
||||
if (expectedDump.rawContent == actualDump) {
|
||||
return
|
||||
}
|
||||
val actualDumpPath: File? = try {
|
||||
FileUtil.createTempFile("actual_${expectedDump.path.fileName}".removeSuffix(".txt"), ".txt").also { it.writeText(actualDump) }
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
throw FileComparisonFailedError(
|
||||
message =
|
||||
"'$moduleName' ${expectedDump.path.fileName} does not match the actual API. " +
|
||||
"If the API change was intentional, please ${if (expectedDump.pathExists) "update" else "create"} '${expectedDump.path}', " +
|
||||
"otherwise revert the change. " + "Simplified diff:\n" + expectedDump.simplifiedBuilder.toString(),
|
||||
expected = expectedDump.rawContent,
|
||||
expectedFilePath = if (expectedDump.pathExists) expectedDump.path.toString() else null,
|
||||
actual = actualDump,
|
||||
actualFilePath = actualDumpPath?.toString())
|
||||
}
|
||||
|
||||
return listOf(
|
||||
{
|
||||
runCheck(primaryExpectedDump, primaryActualBuilder.toString())
|
||||
}, {
|
||||
if (secondaryExpectedDump != null) {
|
||||
runCheck(secondaryExpectedDump, secondaryActualBuilder.toString())
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun appendMembersToActualDump(actualBuilder: StringBuilder, actualMembers: List<String>, className: String, existsInDump: Boolean) {
|
||||
if (actualMembers.isNotEmpty() || existsInDump) {
|
||||
actualBuilder.appendLine(className)
|
||||
actualMembers.forEach { actualBuilder.appendLine(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeLeftoverMethods(serializedMembers: Set<String>?, simplifiedBuilder: StringBuilder?) {
|
||||
if (serializedMembers?.isNotEmpty() == true) {
|
||||
checkNotNull(simplifiedBuilder) { "simplified dump is expected to be not null" }
|
||||
serializedMembers.forEach {
|
||||
simplifiedBuilder.appendLine("- $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupClassesThatDoNotExist(classes: Map<ClassName, ClassMembers>, expectedDump: ExpectedDump) {
|
||||
for ((redundantName, redundantMembers) in classes) {
|
||||
expectedDump.simplifiedBuilder.appendLine("- $redundantName")
|
||||
redundantMembers.forEach { expectedDump.simplifiedBuilder.appendLine("- $it") }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun Array<String>.groupByClasses(): Map<ClassName, ClassMembers> {
|
||||
return fold(mutableMapOf<String, MutableList<String>>() to "") { (acc, className), s ->
|
||||
if (s.isBlank()) {
|
||||
return@fold acc to className
|
||||
}
|
||||
if (!s.startsWith("-")) {
|
||||
acc[s] = mutableListOf()
|
||||
acc to s
|
||||
}
|
||||
else {
|
||||
acc[className]!!.add(s)
|
||||
acc to className
|
||||
}
|
||||
}.first
|
||||
}
|
||||
|
||||
private fun checkExposedApi(moduleName: String, api: API, apiFilter: ApiClassFilter) {
|
||||
val (exposedThirdPartyApi, exposedPrivateApi) = exposedApi(api, apiFilter)
|
||||
assertAll(
|
||||
{ assertExposedApi(exposedThirdPartyApi, "'${moduleName}' Third party classes are exposed through API") },
|
||||
{ assertExposedApi(exposedPrivateApi, "'${moduleName}' Private classes are exposed through API") },
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertExposedApi(exposedThrough: Map<String, List<String>>, message: String) {
|
||||
if (exposedThrough.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val exposedApiString = buildString {
|
||||
for ((exposed, through) in exposedThrough) {
|
||||
for (signature in through) {
|
||||
appendLine("$exposed <- $signature")
|
||||
}
|
||||
}
|
||||
}
|
||||
fail("$message:\n$exposedApiString")
|
||||
}
|
||||
|
||||
internal const val exposedThirdPartyApiFileName = "exposed-third-party-api.txt"
|
||||
internal const val exposedPrivateApiFileName = "exposed-private-api.txt"
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.tools.apiDump.API
|
||||
import com.intellij.tools.apiDump.printFlags
|
||||
import com.intellij.tools.apiDump.referencedFqns
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @return map with an FQN as a key, and a member (class, method, field) which exposes the FQN as a value
|
||||
*/
|
||||
internal fun exposedApi(api: API, filter: ApiClassFilter): ExposedApi {
|
||||
val exposedThirdPartyClasses = TreeMap<String, MutableList<String>>()
|
||||
val exposedPrivateClasses = TreeMap<String, MutableList<String>>()
|
||||
|
||||
fun checkExposure(fqn: String, through: () -> String) {
|
||||
val exposures = when (api.index.isPublicOrUnknown(fqn)) {
|
||||
null -> {
|
||||
if (fqn in filter) {
|
||||
return
|
||||
}
|
||||
exposedThirdPartyClasses
|
||||
}
|
||||
false -> {
|
||||
if (fqn in filter) {
|
||||
return
|
||||
}
|
||||
// A member function exposes a package-local or internal class via its return type or parameter types.
|
||||
exposedPrivateClasses
|
||||
}
|
||||
true -> {
|
||||
return
|
||||
}
|
||||
}
|
||||
exposures.getOrPut(fqn) { ArrayList() }.add(through())
|
||||
}
|
||||
|
||||
for (classData in api.publicApi) {
|
||||
val className = classData.className
|
||||
for (superClassFqn in classData.supers) {
|
||||
checkExposure(fqn = superClassFqn) {
|
||||
className
|
||||
}
|
||||
}
|
||||
for (member in classData.members) {
|
||||
for (referenced in referencedFqns(member.ref.descriptor)) {
|
||||
checkExposure(fqn = referenced) {
|
||||
buildString {
|
||||
if (printFlags(member.flags, isClass = false)) {
|
||||
append(" ")
|
||||
}
|
||||
append(className)
|
||||
append("#")
|
||||
append(member.ref.name)
|
||||
append(" ")
|
||||
append(member.ref.descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ExposedApi(
|
||||
exposedThirdPartyClasses,
|
||||
exposedPrivateClasses,
|
||||
)
|
||||
}
|
||||
|
||||
internal data class ExposedApi(
|
||||
val exposedThirdPartyClasses: Map<String, List<String>>,
|
||||
val exposedPrivateClasses: Map<String, List<String>>,
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.platform.testFramework.core.FileComparisonFailedError
|
||||
import com.intellij.tools.apiDump.packageName
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.inputStream
|
||||
|
||||
internal fun Path.apiClassFilter(): FileApiClassFilter? {
|
||||
if (exists()) {
|
||||
return FileApiClassFilter(this, inputStream().readApiEntries())
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list of non-blank lines which don't start with `#`
|
||||
*/
|
||||
private fun InputStream.readApiEntries(): List<String> {
|
||||
return bufferedReader().useLines {
|
||||
it.filter { line ->
|
||||
!line.isBlank() && !line.startsWith("#")
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ApiClassFilter {
|
||||
|
||||
operator fun contains(className: String): Boolean
|
||||
|
||||
fun andThen(another: ApiClassFilter): ApiClassFilter {
|
||||
if (another == Empty) {
|
||||
return this
|
||||
}
|
||||
return object : ApiClassFilter {
|
||||
override fun contains(className: String): Boolean = className in this@ApiClassFilter || className in another
|
||||
override fun toString(): String = "${this@ApiClassFilter} | $another"
|
||||
}
|
||||
}
|
||||
|
||||
data object Empty : ApiClassFilter {
|
||||
override fun contains(className: String): Boolean = false
|
||||
override fun andThen(another: ApiClassFilter): ApiClassFilter = another
|
||||
}
|
||||
}
|
||||
|
||||
internal class FileApiClassFilter(
|
||||
private val path: Path,
|
||||
private val entries: List<String>,
|
||||
) : ApiClassFilter {
|
||||
|
||||
private val packages: Set<String>
|
||||
private val recursivePackages: Set<String> // TODO use prefix tree
|
||||
private val classes: Set<String>
|
||||
|
||||
init {
|
||||
val packages = HashSet<String>()
|
||||
val recursivePackages = HashSet<String>()
|
||||
val classes = HashSet<String>()
|
||||
for (line in entries) {
|
||||
if (line.endsWith("/*")) {
|
||||
packages.add(line.removeSuffix("*"))
|
||||
}
|
||||
else if (line.endsWith("/**")) {
|
||||
recursivePackages.add(line.removeSuffix("**"))
|
||||
}
|
||||
else {
|
||||
classes.add(line)
|
||||
}
|
||||
}
|
||||
this.packages = packages
|
||||
this.recursivePackages = recursivePackages
|
||||
this.classes = classes
|
||||
}
|
||||
|
||||
private val usedEntries = HashSet<String>()
|
||||
|
||||
override fun toString(): String = path.toString()
|
||||
|
||||
override fun contains(className: String): Boolean {
|
||||
if (className in classes) {
|
||||
usedEntries.add(className)
|
||||
return true
|
||||
}
|
||||
val packageName = className.packageName() + "/"
|
||||
if (packageName in packages) {
|
||||
usedEntries.add(packageName)
|
||||
return true
|
||||
}
|
||||
for (recursivePackage in recursivePackages) {
|
||||
if (packageName.startsWith(recursivePackage)) {
|
||||
usedEntries.add(recursivePackage)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun checkAllEntriesAreUsed() {
|
||||
val unusedEntries = unusedEntries()
|
||||
if (unusedEntries.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val message = buildString {
|
||||
append("$path entries does not match any exposed API. ")
|
||||
appendLine("The following entries are no longer exposed and should be removed from $path:")
|
||||
for (it in unusedEntries) {
|
||||
appendLine(it)
|
||||
}
|
||||
}
|
||||
throw FileComparisonFailedError(
|
||||
message,
|
||||
expected = entries.joinToString("\n", postfix = "\n"),
|
||||
actual = (entries - unusedEntries).joinToString("\n", postfix = "\n"),
|
||||
expectedFilePath = path.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun unusedEntries(): Set<String> {
|
||||
val result = TreeSet<String>()
|
||||
(packages - usedEntries).mapTo(result) { "$it*" }
|
||||
(recursivePackages - usedEntries).mapTo(result) { "$it**" }
|
||||
result.addAll(classes - usedEntries)
|
||||
return result
|
||||
}
|
||||
|
||||
fun checkEntriesSortedAndUnique() {
|
||||
val sorted = entries.toSortedSet().toList()
|
||||
if (sorted == entries) {
|
||||
return
|
||||
}
|
||||
throw FileComparisonFailedError(
|
||||
message = "$path contents are not sorted",
|
||||
expected = sorted.joinToString(separator = "\n", postfix = "\n"),
|
||||
actual = entries.joinToString(separator = "\n", postfix = "\n"),
|
||||
actualFilePath = path.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.platform.testFramework.monorepo.MonorepoProjectStructure
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class ApiDumpFilePresenseTest {
|
||||
@Test
|
||||
fun `platform modules define API dump`() {
|
||||
checkModulesDefineApiDump(MonorepoProjectStructure.communityProject.modules)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.platform.testFramework.monorepo.api.PlatformApi.isPlatformModule
|
||||
import com.intellij.platform.testFramework.monorepo.hasProductionSources
|
||||
import org.jetbrains.jps.model.module.JpsModule
|
||||
import org.junit.jupiter.api.fail
|
||||
import kotlin.io.path.exists
|
||||
|
||||
fun checkModulesDefineApiDump(modules: List<JpsModule>) {
|
||||
val modulesWithoutApiDump = modules
|
||||
.filter { module ->
|
||||
module.isPlatformModule()
|
||||
&& module.hasProductionSources() // skip modules without sources, for example, modules with tests
|
||||
&& module.firstContentRoot().let { contentRoot ->
|
||||
contentRoot != null // skip modules without content roots
|
||||
&& !contentRoot.stableApiDumpPath().exists()
|
||||
}
|
||||
}
|
||||
if (modulesWithoutApiDump.isNotEmpty()) {
|
||||
fail {
|
||||
"Platform modules should define 'api-dump.txt' in the first content root. " +
|
||||
"Modules without API dump:\n" +
|
||||
modulesWithoutApiDump.joinToString("\n") { it.name }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.openapi.application.PathManager
|
||||
import com.intellij.tools.apiDump.API
|
||||
import com.intellij.tools.apiDump.api
|
||||
import com.intellij.tools.apiDump.emptyApiIndex
|
||||
import com.intellij.util.SuspendingLazy
|
||||
import com.intellij.util.suspendingLazy
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.fold
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.jps.ProjectPaths
|
||||
import org.jetbrains.jps.model.java.JpsJavaExtensionService
|
||||
import org.jetbrains.jps.model.module.JpsModule
|
||||
import org.junit.jupiter.api.fail
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.io.path.name
|
||||
|
||||
@ApiStatus.Internal
|
||||
class ModuleApi(private val cs: CoroutineScope) {
|
||||
|
||||
private val knownModules = ConcurrentHashMap<String, SuspendingLazy<API>>()
|
||||
|
||||
suspend fun moduleApi(module: JpsModule): API {
|
||||
return checkNotNull(knownModules[module.name]).getValue()
|
||||
}
|
||||
|
||||
fun discoverModule(module: JpsModule) {
|
||||
knownModules.computeIfAbsent(module.name) {
|
||||
cs.suspendingLazy(CoroutineName(module.name)) {
|
||||
computeModuleApi(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun computeModuleApi(module: JpsModule): API {
|
||||
val dependencies = JpsJavaExtensionService.dependencies(module)
|
||||
.compileOnly()
|
||||
.productionOnly()
|
||||
.modules
|
||||
.filter {
|
||||
knownModules.containsKey(it.name)
|
||||
}
|
||||
val dependencyIndex = channelFlow {
|
||||
for (dependency in dependencies) {
|
||||
launch {
|
||||
val api = moduleApi(dependency)
|
||||
send(api.index)
|
||||
}
|
||||
}
|
||||
}.fold(emptyApiIndex) { acc, item ->
|
||||
acc + item
|
||||
}
|
||||
|
||||
var outputDir = ProjectPaths.getModuleOutputDir(module, false)?.toPath()
|
||||
?: fail("'${module.name}' has no out directory")
|
||||
val mapping = PathManager.getArchivedCompiledClassesMapping()
|
||||
if (mapping != null) {
|
||||
// path is absolute, mapping contains only the last two path elements
|
||||
outputDir = mapping[outputDir.parent.name + "/" + outputDir.name]?.let { Path.of(it) } ?: outputDir
|
||||
}
|
||||
|
||||
return api(dependencyIndex, outputDir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.platform.testFramework.monorepo.MonorepoProjectStructure.isFromCommunity
|
||||
import org.jetbrains.jps.model.module.JpsModule
|
||||
|
||||
internal object PlatformApi {
|
||||
|
||||
private val excludeModuleNames = setOf(
|
||||
"intellij.platform.buildScripts", // build scripts
|
||||
"intellij.platform.testFramework",
|
||||
"intellij.platform.testExtensions",
|
||||
"intellij.platform.uast", // IDEA
|
||||
"intellij.platform.uast.ide", // IDEA
|
||||
"intellij.platform.images", // plugin
|
||||
"intellij.platform.images.copyright", // plugin
|
||||
"intellij.platform.images.build", // build scripts
|
||||
)
|
||||
|
||||
private val excludeModuleNamePrefixes = setOf(
|
||||
"intellij.platform.testFramework.",
|
||||
"intellij.platform.buildScripts.",
|
||||
)
|
||||
|
||||
private val excludeModuleNameSuffixes = setOf(
|
||||
".testFramework",
|
||||
".performanceTesting"
|
||||
)
|
||||
|
||||
fun JpsModule.isPlatformModule(): Boolean {
|
||||
val name = name
|
||||
return name.startsWith("intellij.platform.") &&
|
||||
name !in excludeModuleNames &&
|
||||
excludeModuleNamePrefixes.none(name::startsWith) &&
|
||||
excludeModuleNameSuffixes.none(name::endsWith) &&
|
||||
isFromCommunity()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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.platform.testFramework.monorepo.api
|
||||
|
||||
import com.intellij.platform.testFramework.monorepo.MonorepoProjectStructure
|
||||
import com.intellij.platform.testFramework.monorepo.api.PlatformApi.isPlatformModule
|
||||
import com.intellij.platform.testFramework.monorepo.hasProductionSources
|
||||
import com.intellij.tools.apiDump.dumpApi
|
||||
import com.intellij.util.io.delete
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.jps.model.module.JpsModule
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
/**
|
||||
* Generates and overwrites [MODULE_API_DUMP_FILE_NAME] or [MODULE_API_DUMP_UNREVIEWED_FILE_NAME]
|
||||
* in modules which match [isPlatformModule].
|
||||
*/
|
||||
fun main(): Unit = runBlocking {
|
||||
generateApiDumps(this, MonorepoProjectStructure.communityProject.modules)
|
||||
}
|
||||
|
||||
suspend fun generateApiDumps(coroutineScope: CoroutineScope, wantedModules: List<JpsModule>) {
|
||||
val modules = wantedModules
|
||||
.filter {
|
||||
it.isPlatformModule()
|
||||
|| it.firstContentRoot().let { it != null && (it / MODULE_API_DUMP_FILE_NAME).exists() }
|
||||
}
|
||||
.filter(JpsModule::hasProductionSources)
|
||||
.toList()
|
||||
val moduleApi = ModuleApi(coroutineScope + Dispatchers.Default)
|
||||
for (module in modules) {
|
||||
moduleApi.discoverModule(module)
|
||||
}
|
||||
val exposedThirdPartyApiFilter: FileApiClassFilter = globalExposedThirdPartyClasses(modules)
|
||||
var reviewedModules = 0
|
||||
var privateApiExposures = 0
|
||||
var privateApiExposuresInUnreviewedModules = 0
|
||||
for (module in modules) {
|
||||
val contentRootPath = module.firstContentRoot() ?: continue
|
||||
var reviewed = true
|
||||
var stableApiDumpFilePath = contentRootPath / MODULE_API_DUMP_FILE_NAME
|
||||
if (!stableApiDumpFilePath.exists()) {
|
||||
stableApiDumpFilePath = contentRootPath / MODULE_API_DUMP_UNREVIEWED_FILE_NAME
|
||||
reviewed = false
|
||||
}
|
||||
var experimentalApiDumpFilePath = contentRootPath / MODULE_API_DUMP_EXPERIMENTAL_FILE_NAME
|
||||
println("- [${if (reviewed) "x" else " "}] ${module.name}")
|
||||
val api = moduleApi.moduleApi(module)
|
||||
stableApiDumpFilePath.writeText(dumpApi(api.stableApi))
|
||||
if (api.experimentalApi.isEmpty()) {
|
||||
experimentalApiDumpFilePath.delete()
|
||||
}
|
||||
else {
|
||||
experimentalApiDumpFilePath.writeText(dumpApi(api.experimentalApi))
|
||||
}
|
||||
val (exposedThirdPartyClasses, exposedPrivateClasses) = exposedApi(api, exposedThirdPartyApiFilter)
|
||||
listExposures(exposedThirdPartyClasses, contentRootPath / exposedThirdPartyApiFileName)
|
||||
listExposures(exposedPrivateClasses, contentRootPath / exposedPrivateApiFileName)
|
||||
if (reviewed) {
|
||||
reviewedModules++
|
||||
privateApiExposures += exposedPrivateClasses.values.sumOf { it.size }
|
||||
}
|
||||
else {
|
||||
privateApiExposuresInUnreviewedModules += exposedPrivateClasses.values.sumOf { it.size }
|
||||
}
|
||||
}
|
||||
println("Reviewed modules: $reviewedModules of ${modules.size}")
|
||||
println("Exposed private API in reviewed/unreviewed modules: $privateApiExposures/$privateApiExposuresInUnreviewedModules")
|
||||
}
|
||||
|
||||
private fun listExposures(exposures: Map<String, *>, out: Path) {
|
||||
if (exposures.isEmpty()) {
|
||||
out.delete()
|
||||
}
|
||||
else {
|
||||
out.writeText(
|
||||
exposures.keys.sorted().joinToString(separator = "\n", postfix = "\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user