[FIR] Correctly process slash in class name in ClassId

^KTIJ-27358 fixed


Merge-request: KT-MR-12673
Merged-by: Egor Kulikov <Egor.Kulikov@jetbrains.com>
This commit is contained in:
Egor Kulikov
2023-11-07 09:47:05 +00:00
committed by Space Team
parent 1173cc46e2
commit 408e2f109f
6 changed files with 77 additions and 7 deletions
@@ -0,0 +1,2 @@
/* ClassId: /`test/` */class `test/`
/* ClassId: /`test/test` */class `test/test`
@@ -5,21 +5,28 @@
package org.jetbrains.kotlin.analysis.low.level.api.fir 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.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.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.AnalysisApiFirScriptTestConfigurator
import org.jetbrains.kotlin.analysis.low.level.api.fir.test.configurators.AnalysisApiFirSourceTestConfigurator 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.test.configurators.AnalysisApiTestConfigurator
import org.jetbrains.kotlin.analysis.test.framework.utils.ignoreExceptionIfIgnoreDirectivePresent 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.KtClassLikeDeclaration
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.safeFqNameForLazyResolve 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.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.services.TestModuleStructure import org.jetbrains.kotlin.test.services.TestModuleStructure
import org.jetbrains.kotlin.test.services.TestServices import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.assertions import org.jetbrains.kotlin.test.services.assertions
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
abstract class AbstractClassIdConsistencyTest : AbstractLowLevelApiSingleFileTest() { abstract class AbstractClassIdConsistencyTest : AbstractLowLevelApiSingleFileTest() {
override fun doTestByFileStructure(ktFile: KtFile, moduleStructure: TestModuleStructure, testServices: TestServices) { override fun doTestByFileStructure(ktFile: KtFile, moduleStructure: TestModuleStructure, testServices: TestServices) {
@@ -28,10 +35,49 @@ abstract class AbstractClassIdConsistencyTest : AbstractLowLevelApiSingleFileTes
val classId = declaration.getClassId() val classId = declaration.getClassId()
val fqName = declaration.safeFqNameForLazyResolve() val fqName = declaration.safeFqNameForLazyResolve()
testServices.assertions.assertEquals(fqName, classId?.asSingleFqName()) 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<String?> = mutableListOf(null)
var map: MutableMap<String?, Int> = 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() { private object Directives : SimpleDirectivesContainer() {
val IGNORE_CONSISTENCY_CHECK by stringDirective("Temporary disable test until the issue is fixed") val IGNORE_CONSISTENCY_CHECK by stringDirective("Temporary disable test until the issue is fixed")
} }
@@ -42,6 +42,12 @@ public class SourceClassIdConsistencyTestGenerated extends AbstractSourceClassId
runTest("analysis/low-level-api-fir/testData/classId/enumEntry.kt"); 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 @Test
@TestMetadata("localClassInsideFunctionLiteral.kt") @TestMetadata("localClassInsideFunctionLiteral.kt")
public void testLocalClassInsideFunctionLiteral() throws Exception { public void testLocalClassInsideFunctionLiteral() throws Exception {
@@ -42,6 +42,12 @@ public class SourceClassIdTestGenerated extends AbstractSourceClassIdTest {
runTest("analysis/low-level-api-fir/testData/classId/enumEntry.kt"); 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 @Test
@TestMetadata("localClassInsideFunctionLiteral.kt") @TestMetadata("localClassInsideFunctionLiteral.kt")
public void testLocalClassInsideFunctionLiteral() throws Exception { public void testLocalClassInsideFunctionLiteral() throws Exception {
@@ -12,7 +12,7 @@ object KotlinStubVersions {
// Though only kotlin declarations (no code in the bodies) are stubbed, please do increase this version // 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. // 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. // 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 // 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). // or changes are made to the core stub building code (org.jetbrains.kotlin.idea.decompiler.stubBuilder).
@@ -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" * @return a string where packages are delimited by '/' and classes by '.', e.g. "kotlin/Map.Entry"
*/ */
fun asString(): String { fun asString(): String {
fun FqName.escapeSlashes(): String {
val res = asString()
if (res.contains('/')) {
return "`$res`"
}
return res
}
return if (packageFqName.isRoot) { return if (packageFqName.isRoot) {
relativeClassName.asString() relativeClassName.escapeSlashes()
} else { } else {
buildString { buildString {
append(packageFqName.asString().replace('.', '/')) append(packageFqName.asString().replace('.', '/'))
append("/") 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 @JvmOverloads
@JvmStatic @JvmStatic
fun fromString(string: String, isLocal: Boolean = false): ClassId { 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 packageName: String
val className: String val className: String
if (lastSlashIndex == -1) { if (lastSlashIndex == -1) {
packageName = "" packageName = ""
className = string className = string.replace("`", "")
} else { } else {
packageName = string.substring(0, lastSlashIndex).replace('/', '.') packageName = string.substring(0, lastSlashIndex).replace('/', '.')
className = string.substring(lastSlashIndex + 1) className = string.substring(lastSlashIndex + 1).replace("`", "")
} }
return ClassId(FqName(packageName), FqName(className), isLocal) return ClassId(FqName(packageName), FqName(className), isLocal)
} }