diff --git a/analysis/low-level-api-fir/testData/classId/invalidForJvmClassName.kt b/analysis/low-level-api-fir/testData/classId/invalidForJvmClassName.kt new file mode 100644 index 00000000000..41bd3349536 --- /dev/null +++ b/analysis/low-level-api-fir/testData/classId/invalidForJvmClassName.kt @@ -0,0 +1,2 @@ +/* ClassId: /`test/` */class `test/` +/* ClassId: /`test/test` */class `test/test` diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/AbstractClassIdConsistencyTest.kt b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/AbstractClassIdConsistencyTest.kt index f1384f6f0b8..2b5f26de294 100644 --- a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/AbstractClassIdConsistencyTest.kt +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/AbstractClassIdConsistencyTest.kt @@ -5,21 +5,28 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir +import com.intellij.psi.stubs.StubInputStream +import com.intellij.psi.stubs.StubOutputStream +import com.intellij.util.io.AbstractStringEnumerator import org.jetbrains.kotlin.analysis.low.level.api.fir.AbstractClassIdConsistencyTest.Directives.IGNORE_CONSISTENCY_CHECK import org.jetbrains.kotlin.analysis.low.level.api.fir.test.base.AbstractLowLevelApiSingleFileTest import org.jetbrains.kotlin.analysis.low.level.api.fir.test.configurators.AnalysisApiFirScriptTestConfigurator import org.jetbrains.kotlin.analysis.low.level.api.fir.test.configurators.AnalysisApiFirSourceTestConfigurator import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator import org.jetbrains.kotlin.analysis.test.framework.utils.ignoreExceptionIfIgnoreDirectivePresent +import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.psi.KtClassLikeDeclaration import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType import org.jetbrains.kotlin.psi.psiUtil.safeFqNameForLazyResolve +import org.jetbrains.kotlin.psi.stubs.StubUtils import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer import org.jetbrains.kotlin.test.services.TestModuleStructure import org.jetbrains.kotlin.test.services.TestServices import org.jetbrains.kotlin.test.services.assertions +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream abstract class AbstractClassIdConsistencyTest : AbstractLowLevelApiSingleFileTest() { override fun doTestByFileStructure(ktFile: KtFile, moduleStructure: TestModuleStructure, testServices: TestServices) { @@ -28,10 +35,49 @@ abstract class AbstractClassIdConsistencyTest : AbstractLowLevelApiSingleFileTes val classId = declaration.getClassId() val fqName = declaration.safeFqNameForLazyResolve() testServices.assertions.assertEquals(fqName, classId?.asSingleFqName()) + + testSerialization(classId, testServices) } } } + private fun testSerialization(classId: ClassId?, testServices: TestServices) { + val outStream = ByteArrayOutputStream() + + val enumerator = object : AbstractStringEnumerator { + var list: MutableList = mutableListOf(null) + var map: MutableMap = mutableMapOf(null to 0) + + override fun close() { + } + + override fun isDirty() = false + + override fun force() { + } + + override fun enumerate(value: String?): Int { + return map.getOrElse(value) { + map[value] = list.size + list += value + list.size - 1 + } + } + + override fun valueOf(idx: Int): String? { + return list[idx] + } + + override fun markCorrupted() { + } + } + + StubUtils.serializeClassId(StubOutputStream(outStream, enumerator), classId) + val inStream = ByteArrayInputStream(outStream.toByteArray()) + val result = StubUtils.deserializeClassId(StubInputStream(inStream, enumerator)) + testServices.assertions.assertEquals(classId, result) + } + private object Directives : SimpleDirectivesContainer() { val IGNORE_CONSISTENCY_CHECK by stringDirective("Temporary disable test until the issue is fixed") } diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/SourceClassIdConsistencyTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/SourceClassIdConsistencyTestGenerated.java index a85a89b0bbd..b5dcd7e3e0a 100644 --- a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/SourceClassIdConsistencyTestGenerated.java +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/SourceClassIdConsistencyTestGenerated.java @@ -42,6 +42,12 @@ public class SourceClassIdConsistencyTestGenerated extends AbstractSourceClassId runTest("analysis/low-level-api-fir/testData/classId/enumEntry.kt"); } + @Test + @TestMetadata("invalidForJvmClassName.kt") + public void testInvalidForJvmClassName() throws Exception { + runTest("analysis/low-level-api-fir/testData/classId/invalidForJvmClassName.kt"); + } + @Test @TestMetadata("localClassInsideFunctionLiteral.kt") public void testLocalClassInsideFunctionLiteral() throws Exception { diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/SourceClassIdTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/SourceClassIdTestGenerated.java index ef0dd0d9f0f..3f610f058fc 100644 --- a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/SourceClassIdTestGenerated.java +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/SourceClassIdTestGenerated.java @@ -42,6 +42,12 @@ public class SourceClassIdTestGenerated extends AbstractSourceClassIdTest { runTest("analysis/low-level-api-fir/testData/classId/enumEntry.kt"); } + @Test + @TestMetadata("invalidForJvmClassName.kt") + public void testInvalidForJvmClassName() throws Exception { + runTest("analysis/low-level-api-fir/testData/classId/invalidForJvmClassName.kt"); + } + @Test @TestMetadata("localClassInsideFunctionLiteral.kt") public void testLocalClassInsideFunctionLiteral() throws Exception { diff --git a/compiler/psi/src/org/jetbrains/kotlin/psi/stubs/KotlinStubVersions.kt b/compiler/psi/src/org/jetbrains/kotlin/psi/stubs/KotlinStubVersions.kt index 6235d6f1f85..2c21f106d47 100644 --- a/compiler/psi/src/org/jetbrains/kotlin/psi/stubs/KotlinStubVersions.kt +++ b/compiler/psi/src/org/jetbrains/kotlin/psi/stubs/KotlinStubVersions.kt @@ -12,7 +12,7 @@ object KotlinStubVersions { // Though only kotlin declarations (no code in the bodies) are stubbed, please do increase this version // if you are not 100% sure it can be avoided. // Increasing this version will lead to reindexing of all kotlin source files on the first IDE startup with the new version. - const val SOURCE_STUB_VERSION = 157 + const val SOURCE_STUB_VERSION = 158 // Binary stub version should be increased if stub format (org.jetbrains.kotlin.psi.stubs.impl) is changed // or changes are made to the core stub building code (org.jetbrains.kotlin.idea.decompiler.stubBuilder). diff --git a/core/compiler.common/src/org/jetbrains/kotlin/name/ClassId.kt b/core/compiler.common/src/org/jetbrains/kotlin/name/ClassId.kt index 0a9d00c8ed3..7c61820ed17 100644 --- a/core/compiler.common/src/org/jetbrains/kotlin/name/ClassId.kt +++ b/core/compiler.common/src/org/jetbrains/kotlin/name/ClassId.kt @@ -75,13 +75,21 @@ data class ClassId(val packageFqName: FqName, val relativeClassName: FqName, val * @return a string where packages are delimited by '/' and classes by '.', e.g. "kotlin/Map.Entry" */ fun asString(): String { + fun FqName.escapeSlashes(): String { + val res = asString() + if (res.contains('/')) { + return "`$res`" + } + return res + } + return if (packageFqName.isRoot) { - relativeClassName.asString() + relativeClassName.escapeSlashes() } else { buildString { append(packageFqName.asString().replace('.', '/')) append("/") - append(relativeClassName.asString()) + append(relativeClassName.escapeSlashes()) } } } @@ -109,20 +117,22 @@ data class ClassId(val packageFqName: FqName, val relativeClassName: FqName, val } /** - * @param string a string where packages are delimited by '/' and classes by '.', e.g. "kotlin/Map.Entry" + * @param string a string where packages are delimited by '/' and classes by '.', e.g. "kotlin/Map.Entry". + * If class name contains slashes, it should be put into ticks, e.g. "package/`test/test`" */ @JvmOverloads @JvmStatic fun fromString(string: String, isLocal: Boolean = false): ClassId { - val lastSlashIndex = string.lastIndexOf("/") + val tickIndex = string.indexOf('`') + val lastSlashIndex = string.lastIndexOf("/", if (tickIndex == -1) string.length else tickIndex) val packageName: String val className: String if (lastSlashIndex == -1) { packageName = "" - className = string + className = string.replace("`", "") } else { packageName = string.substring(0, lastSlashIndex).replace('/', '.') - className = string.substring(lastSlashIndex + 1) + className = string.substring(lastSlashIndex + 1).replace("`", "") } return ClassId(FqName(packageName), FqName(className), isLocal) }