KT-45777: Take snapshots and compute changes for Java classes

Create `JavaClassDescriptor`s for Java classes
Ignore anonymous and synthetic classes
as they can't impact recompilation.
Clean up handling of local and anonymous classes

Bug: KT-45777
Test: New JavaClassDescriptorCreatorTest
This commit is contained in:
Hung Nguyen
2021-08-11 09:22:39 +01:00
committed by nataliya.valtman
parent 98e4d67900
commit a342c81a9f
25 changed files with 2993 additions and 197 deletions
@@ -118,7 +118,12 @@ abstract class AbstractIncrementalCache<ClassName>(
}
}
protected fun addToClassStorage(proto: ProtoBuf.Class, nameResolver: NameResolver, srcFile: File) {
/**
* Updates class storage based on the given class proto.
*
* The `srcFile` argument may be `null` (e.g., if we are processing .class files in jars where source files are not available).
*/
protected fun addToClassStorage(proto: ProtoBuf.Class, nameResolver: NameResolver, srcFile: File?) {
val supertypes = proto.supertypes(TypeTable(proto.typeTable))
val parents = supertypes.map { nameResolver.getClassId(it.className).asSingleFqName() }
.filter { it.asString() != "kotlin.Any" }
@@ -131,7 +136,7 @@ abstract class AbstractIncrementalCache<ClassName>(
removedSupertypes.forEach { subtypesMap.removeValues(it, setOf(child)) }
supertypesMap[child] = parents
classFqNameToSourceMap[child] = srcFile
srcFile?.let { classFqNameToSourceMap[child] = it }
classAttributesMap[child] = ICClassesAttributes(ProtoBuf.Modality.SEALED == Flags.MODALITY.get(proto.flags))
}
@@ -184,7 +184,9 @@ open class IncrementalJvmCache(
KotlinClassHeader.Kind.CLASS -> {
if (sourceFiles != null) {
assert(sourceFiles.size == 1) { "Class is expected to have only one source file: $sourceFiles" }
addToClassStorage(kotlinClassInfo, sourceFiles.first())
addToClassStorage(kotlinClassInfo, sourceFiles.single())
} else {
addToClassStorage(kotlinClassInfo, null)
}
protoMap.process(kotlinClassInfo, changesCollector)
@@ -196,10 +198,10 @@ open class IncrementalJvmCache(
}
}
fun saveJavaClassProto(source: File, serializedJavaClass: SerializedJavaClass, collector: ChangesCollector) {
fun saveJavaClassProto(source: File?, serializedJavaClass: SerializedJavaClass, collector: ChangesCollector) {
val jvmClassName = JvmClassName.byClassId(serializedJavaClass.classId)
javaSourcesProtoMap.process(jvmClassName, serializedJavaClass, collector)
sourceToClassesMap.add(source, jvmClassName)
source?.let { sourceToClassesMap.add(source, jvmClassName) }
val (proto, nameResolver) = serializedJavaClass.toProtoData()
addToClassStorage(proto, nameResolver, source)
// collector.addJavaProto(ClassProtoData(proto, nameResolver))
@@ -501,7 +503,7 @@ open class IncrementalJvmCache(
value.dumpCollection()
}
private fun addToClassStorage(classInfo: KotlinClassInfo, srcFile: File) {
private fun addToClassStorage(classInfo: KotlinClassInfo, srcFile: File?) {
val (nameResolver, proto) = JvmProtoBufUtil.readClassDataFrom(classInfo.classHeaderData, classInfo.classHeaderStrings)
addToClassStorage(proto, nameResolver, srcFile)
}
@@ -0,0 +1,225 @@
/*
* Copyright 2010-2021 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.incremental
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.SourceFile
import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies
import org.jetbrains.kotlin.load.java.JavaClassFinder
import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor
import org.jetbrains.kotlin.load.java.sources.JavaSourceElement
import org.jetbrains.kotlin.load.java.sources.JavaSourceElementFactory
import org.jetbrains.kotlin.load.java.structure.JavaAnnotation
import org.jetbrains.kotlin.load.java.structure.JavaClass
import org.jetbrains.kotlin.load.java.structure.JavaElement
import org.jetbrains.kotlin.load.java.structure.JavaPackage
import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryClassSignatureParser
import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass
import org.jetbrains.kotlin.load.java.structure.impl.classFiles.ClassifierResolutionContext
import org.jetbrains.kotlin.load.kotlin.DeserializationComponentsForJava.Companion.createModuleData
import org.jetbrains.kotlin.load.kotlin.KotlinClassFinder
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.serialization.DescriptorSerializer
import org.jetbrains.kotlin.serialization.deserialization.ErrorReporter
import org.jetbrains.kotlin.serialization.deserialization.builtins.BuiltInSerializerProtocol
import org.jetbrains.kotlin.serialization.deserialization.builtins.BuiltInsResourceLoader
import java.io.InputStream
/** Creates [JavaClassDescriptor]s of Java classes. */
object JavaClassDescriptorCreator {
/**
* Creates [JavaClassDescriptor]s of the given Java classes.
*
* Note that creating a [JavaClassDescriptor] for a nested class will require accessing the outer class (and possibly vice versa).
* Therefore, outer classes and nested classes must be passed together in one invocation of this method.
*/
fun create(classIds: List<ClassId>, classesContents: List<ByteArray>): List<JavaClassDescriptor> {
val binaryJavaClasses = classIds.mapIndexed { index, classId ->
createBinaryJavaClass(classId, classesContents[index])
}
val moduleDescriptor = createModuleData(
kotlinClassFinder = NoOpKotlinClassFinder,
jvmBuiltInsKotlinClassFinder = JvmBuiltInsKotlinClassFinder(),
javaClassFinder = BinaryJavaClassFinder(binaryJavaClasses),
moduleName = JavaClassDescriptorCreator::class.java.simpleName,
errorReporter = ThrowImmediatelyErrorReporter,
javaSourceElementFactory = NoSourceJavaSourceElementFactory
).deserializationComponentsForJava.components.moduleDescriptor
return classIds.map { classId ->
moduleDescriptor.findClassAcrossModuleDependencies(classId) as? JavaClassDescriptor
?: error("Failed to create JavaClassDescriptor for class '$classId'")
}
}
}
private fun createBinaryJavaClass(classId: ClassId, classContents: ByteArray): BinaryJavaClass {
val context = ClassifierResolutionContext {
null // TODO: Implement this?
}
val outerClass: JavaClass? = null // TODO: Compute outer class?
val innerClassFinder: ((Name) -> JavaClass?) = { _ ->
null // TODO: Implement this?
}
return try {
BinaryJavaClass(
virtualFile = null,
fqName = classId.asSingleFqName(),
context = context,
signatureParser = BinaryClassSignatureParser(),
access = 0,
outerClass = outerClass,
classContent = classContents,
innerClassFinder = innerClassFinder
)
} catch (e: NoSuchMethodError) {
// When running unit tests, the above call currently fails as JavaClassDescriptorCreator and BinaryJavaClass are located in
// different jars (kotlin-build-common.jar and kotlin-compiler-embeddable.jar), and in the second jar, `com.intellij.*` classes are
// repackaged as `org.jetbrains.kotlin.com.intellij.*`. The `virtualFile` argument above (even though it is null) has type
// `com.intellij.openapi.vfs.VirtualFile`, which has been repackaged, so the method signatures won't match at run time.
// In integrations tests or regular builds, JavaClassDescriptorCreator and BinaryJavaClass are both located in
// kotlin-compiler-embeddable.jar with all references renamed together, so there are no issues.
// We use reflection here as a workaround for unit tests.
val binaryJavaClass = BinaryJavaClass::class.java
val constructor = binaryJavaClass.constructors.sortedBy { it.parameters.size }[0]
return constructor.newInstance(
/* virtualFile */ null,
/* fqName */ classId.asSingleFqName(),
/* context */ context,
/* signatureParser */ BinaryClassSignatureParser(),
/* access */ 0,
/* outerClass */ outerClass,
/* classContent */ classContents,
/* innerClassFinder */ innerClassFinder
) as BinaryJavaClass
}
}
/**
* [JavaClassFinder] that returns results based on the given [BinaryJavaClass] list.
*
* Note that some returned results are fake/empty (similar to ReflectJavaClassFinder).
* TODO: Revise and see if there are any correctness issues.
*/
private class BinaryJavaClassFinder(binaryJavaClasses: List<BinaryJavaClass>) : JavaClassFinder {
private val nameToJavaClass: Map<FqName, BinaryJavaClass> = binaryJavaClasses.associateBy { it.fqName }
override fun findClass(request: JavaClassFinder.Request): JavaClass? {
return nameToJavaClass[request.classId.asSingleFqName()]
}
override fun findPackage(fqName: FqName): JavaPackage {
return object : JavaPackage {
override val fqName: FqName
get() = fqName
override val subPackages: Collection<JavaPackage>
get() = emptyList()
override val annotations: Collection<JavaAnnotation>
get() = emptyList()
override val isDeprecatedInJavaDoc: Boolean
get() = false
override fun getClasses(nameFilter: (Name) -> Boolean): Collection<JavaClass> = emptyList()
override fun findAnnotation(fqName: FqName): JavaAnnotation? = null
}
}
override fun knownClassNamesInPackage(packageFqName: FqName): Set<String>? = null
}
/** [KotlinClassFinder] that returns no results (e.g., when we know that the classes are not Kotlin classes). */
private object NoOpKotlinClassFinder : KotlinClassFinder {
override fun findKotlinClassOrContent(classId: ClassId): KotlinClassFinder.Result? = null
override fun findKotlinClassOrContent(javaClass: JavaClass): KotlinClassFinder.Result? = null
override fun findMetadata(classId: ClassId): InputStream? = null
override fun hasMetadataPackage(fqName: FqName): Boolean = false
override fun findBuiltInsData(packageFqName: FqName): InputStream? = null
}
/** [KotlinClassFinder] for Kotlin JVM built-in classes. */
private class JvmBuiltInsKotlinClassFinder : KotlinClassFinder {
override fun findKotlinClassOrContent(classId: ClassId): KotlinClassFinder.Result? = null
override fun findKotlinClassOrContent(javaClass: JavaClass): KotlinClassFinder.Result? = null
override fun findMetadata(classId: ClassId): InputStream? = null
override fun hasMetadataPackage(fqName: FqName): Boolean = false
private val builtInsResourceLoader by lazy { BuiltInsResourceLoader() }
override fun findBuiltInsData(packageFqName: FqName): InputStream? {
// Same as ReflectKotlinClassFinder.findBuiltInsData
return if (packageFqName.startsWith(StandardNames.BUILT_INS_PACKAGE_NAME)) {
builtInsResourceLoader.loadResource(BuiltInSerializerProtocol.getBuiltInsFilePath(packageFqName))
} else null
}
}
/** [ErrorReporter] that throws errors as soon as they are reported. */
private object ThrowImmediatelyErrorReporter : ErrorReporter {
override fun reportIncompleteHierarchy(descriptor: ClassDescriptor, unresolvedSuperClasses: MutableList<String>) {
// BinaryJavaClassFinder doesn't have the whole classpath so it is unable to resolve classes such as java.lang.Object.
// Ignore this error for now.
// TODO: Revisit later to see how we can address this or if we can safely ignore this error.
}
override fun reportCannotInferVisibility(descriptor: CallableMemberDescriptor) {
error("Cannot infer visibility for $descriptor")
}
}
/** [JavaSourceElementFactory] that doesn't provide a source file for a [JavaElement] (e.g., because the source file is not available). */
private object NoSourceJavaSourceElementFactory : JavaSourceElementFactory {
private class NoSourceJavaElement(override val javaElement: JavaElement) : JavaSourceElement {
override fun getContainingFile(): SourceFile = SourceFile.NO_SOURCE_FILE
}
override fun source(javaElement: JavaElement): JavaSourceElement = NoSourceJavaElement(javaElement)
}
/** Similar to [JavaClassDescriptor.convertToProto] but without an associated source file. */
fun JavaClassDescriptor.toSerializedJavaClass(): SerializedJavaClass {
val extension = JavaClassesSerializerExtension()
val descriptorSerializer = try {
DescriptorSerializer.create(
descriptor = this,
extension = extension,
parentSerializer = null,
project = null
)
} catch (e: NoSuchMethodError) {
// When running unit tests, the above call currently fails as the `project` argument above has type
// `com.intellij.openapi.project.Project`, which is repackaged as `org.jetbrains.kotlin.com.intellij.openapi.project.Project` in
// kotlin-compiler-embeddable.jar (see the comment at the createBinaryJavaClass method for more context).
// We use reflection here as a workaround for unit tests.
val createMethod = DescriptorSerializer::class.java.methods.single { it.name == "create" }
createMethod.invoke(
/* static method */ null,
/* descriptor */ this,
/* extension */ extension,
/* parentSerializer */ null,
/* project */ null
) as DescriptorSerializer
}
val classProto = descriptorSerializer.classProto(this).build()
val (stringTable, qualifiedNameTable) = extension.stringTable.buildProto()
return SerializedJavaClass(classProto, stringTable, qualifiedNameTable)
}
@@ -0,0 +1,210 @@
/*
* Copyright 2010-2021 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.incremental
import com.intellij.openapi.util.Ref
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
/**
* Information about the name of a compiled Java class regarding its nesting level.
*
* A [JavaClassName] can be:
* - [TopLevelClass]
* - [NestedClass] (https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html)
*
* A [NestedClass] can be:
* - [NestedNonLocalClass]
* - [LocalClass] (https://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html)
*/
sealed class JavaClassName(
/** The full name of this class (e.g., "com/example/Foo$Bar"). */
val name: String
) {
/** The package name of this class (e.g., "com/example"). */
val packageName: String
get() = name.substringBeforeLast('/', "")
companion object {
fun compute(classContents: ByteArray): JavaClassName {
val nameRef = Ref.create<String>()
val isTopLevelRef = Ref.create<Boolean>()
val outerNameRef = Ref.create<String>()
val isAnonymousInnerClassRef = Ref.create<Boolean>()
val isSyntheticInnerClassRef = Ref.create<Boolean>()
ClassReader(classContents).accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visit(
version: Int, access: Int, name: String,
signature: String?, superName: String?, interfaces: Array<String?>?
) {
nameRef.set(name)
}
override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) {
if (name == nameRef.get()!!) {
isTopLevelRef.set(false)
outerNameRef.set(outerName)
isAnonymousInnerClassRef.set(innerName == null)
isSyntheticInnerClassRef.set((access and Opcodes.ACC_SYNTHETIC) != 0)
}
}
}, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
val name = nameRef.get()!!
val isTopLevel = isTopLevelRef.get() ?: true
val outerName = outerNameRef.get()
return when {
isTopLevel -> TopLevelClass(name)
outerName != null -> NestedNonLocalClass(
name,
outerName,
isAnonymousInnerClassRef.get()!!,
isSyntheticInnerClassRef.get()!!
)
else -> LocalClass(name)
}
}
}
}
/** See [JavaClassName]. */
class TopLevelClass(name: String) : JavaClassName(name) {
/**
* The simple name of this class (e.g., the simple name of class "com/example/Foo" is "Foo", the simple name of class
* "com/example/ClassWith$Sign" is "ClassWith$Sign").
*/
val simpleName: String
get() = name.substringAfterLast('/')
}
/** See [JavaClassName]. */
sealed class NestedClass(name: String) : JavaClassName(name)
/** See [JavaClassName]. */
class NestedNonLocalClass(
name: String,
/**
* The full name of the outer class of this class (e.g., the outer name of "com/example/OuterClass$NestedClass" is
* "com/example/OuterClass", the outer name of class "com/example/OuterClassWith$Sign$NestedClassWith$Sign" is
* "com/example/OuterClassWith$Sign").
*
* The outer class can be of any type ([TopLevelClass], [NestedNonLocalClass], or [LocalClass]).
*/
val outerName: String,
/**
* Whether this class is an anonymous class.
*
* Note: Even though an anonymous class has no name in the source code, it always has a (not-null, not-empty) name in the compiled
* class. Therefore, [simpleName] is not `null` and not empty even for anonymous classes.
* */
val isAnonymous: Boolean,
/** Whether this class is a synthetic class. */
val isSynthetic: Boolean
) : NestedClass(name) {
init {
check(name.startsWith("$outerName\$"))
}
/**
* The simple name of this class (e.g., the simple name of "com/example/OuterClass$NestedClass" is "NestedClass", the simple name of
* class "com/example/OuterClassWith$Sign$NestedClassWith$Sign" is "NestedClassWith$Sign").
*
* Note: [simpleName] is not `null` and not empty even for anonymous classes (see [isAnonymous]).
*/
val simpleName: String
get() = name.substring("$outerName\$".length)
}
/** See [JavaClassName]. */
class LocalClass(name: String) : NestedClass(name)
/**
* Computes [ClassId]s of the given Java classes.
*
* Note that creating a [ClassId] for a nested class will require accessing the outer class for 2 reasons:
* - To disambiguate any '$' characters in the class name (e.g., "com/example/OuterClassWith$Sign$NestedClassWith$Sign").
* - To determine whether a class is a local class (a nested class of a local class is also considered local, see [ClassId]'s kdoc).
*
* Therefore, outer classes and nested classes must be passed together in one invocation of this method.
*/
fun computeJavaClassIds(javaClassNames: List<JavaClassName>): List<ClassId> {
val nameToJavaClassName: Map<String, JavaClassName> = javaClassNames.associateBy { it.name }
val nameToClassId: MutableMap<String, ClassId?> = nameToJavaClassName.mapValues { null }.toMutableMap()
fun getOrCreateClassId(className: String): ClassId {
val classInfo = nameToJavaClassName[className] ?: error("Class name not found: $className")
val computedClassId = nameToClassId[className]
if (computedClassId != null) {
return computedClassId
}
val packageName = FqName(classInfo.packageName.replace('/', '.'))
val classId = when (classInfo) {
is TopLevelClass -> {
ClassId(packageName, FqName(classInfo.simpleName), /* local */ false)
}
is NestedNonLocalClass -> {
val outerClassId = getOrCreateClassId(classInfo.outerName)
val relativeClassName = FqName(outerClassId.relativeClassName.asString() + "." + classInfo.simpleName)
// For ClassId, a nested non-local class of a local class is also considered local (see ClassId's kdoc).
val isLocal = outerClassId.isLocal
ClassId(packageName, relativeClassName, /* local */ isLocal)
}
is LocalClass -> {
// Note: The following computation for the relative class name of a local class does not exactly match the description given
// in ClassId's kdoc, which currently says "In the case of a local class, relativeClassName consists of a single name
// including all callables' and class' names all the way up to the package, separated by dollar signs."
//
// For example, consider this Java source:
// package com.example;
// class Foo {
// void someMethod() {
// class Bar {
// }
// }
// }
// The above class will compile into class "com/example/Foo" and "com/example/Foo$1Bar" (or
// "com/example/Foo$SomeOtherArbitraryUniqueName which need not contain the string "Bar").
//
// Given that class, the difference between the computed ClassId and the expected ClassId is as follows:
// Value computed below Expected value as defined in ClassId's kdoc
// relativeClassName Foo$1Bar Foo$someMethod$Bar
// classId com.example.Foo$1Bar com.example.Foo$someMethod$Bar
//
// Note: While they don't match, the ClassId computed below is still unique, so it can still be used as an identifier.
//
// TODO: Compute ClassID to match the expected value. It will require collecting information about the enclosing class,
// enclosing method, and simple class name (as written in source code). There will still be a challenge if the class is an
// anonymous class: The simple class name is not available, and it's not clear what the expected ClassID is.
//
// Alternatively, check if we can safely adjust the definition of ClassId for a local class and update any related code
// accordingly.
val relativeClassName = FqName(classInfo.name.substringAfterLast('/'))
ClassId(packageName, relativeClassName, /* local */ true)
}
}
return classId.also {
nameToClassId[className] = it
}
}
return javaClassNames.map { getOrCreateClassId(it.name) }
}
@@ -187,7 +187,6 @@ object StringExternalizer : DataExternalizer<String> {
override fun read(input: DataInput): String = IOUtil.readString(input)
}
// Should be consistent with org.jetbrains.jps.incremental.storage.PathStringDescriptor for correct work of portable caches
object PathStringDescriptor : EnumeratorStringDescriptor() {
private const val PORTABLE_CACHES_PROPERTY = "org.jetbrains.jps.portable.caches"
@@ -239,6 +238,37 @@ fun DataOutput.writeString(value: String) = StringExternalizer.save(this, value)
fun DataInput.readString(): String = StringExternalizer.read(this)
class NullableValueExternalizer<T>(private val valueExternalizer: DataExternalizer<T>) : DataExternalizer<T> {
override fun save(output: DataOutput, value: T?) {
output.writeBoolean(value != null)
value?.let {
valueExternalizer.save(output, it)
}
}
override fun read(input: DataInput): T? {
return if (input.readBoolean()) {
valueExternalizer.read(input)
} else null
}
}
object ByteArrayExternalizer : DataExternalizer<ByteArray> {
override fun save(output: DataOutput, bytes: ByteArray) {
output.writeInt(bytes.size)
output.write(bytes)
}
override fun read(input: DataInput): ByteArray {
val size = input.readInt()
return ByteArray(size).also {
input.readFully(it, 0, size)
}
}
}
class ListExternalizer<T>(
private val elementExternalizer: DataExternalizer<T>
) : DataExternalizer<List<T>> {
@@ -284,19 +314,3 @@ class LinkedHashMapExternalizer<K, V>(
return map
}
}
class NullableValueExternalizer<T>(private val valueExternalizer: DataExternalizer<T>) : DataExternalizer<T> {
override fun save(output: DataOutput, value: T?) {
output.writeBoolean(value != null)
value?.let {
valueExternalizer.save(output, it)
}
}
override fun read(input: DataInput): T? {
return if (input.readBoolean()) {
valueExternalizer.read(input)
} else null
}
}
@@ -0,0 +1,149 @@
/*
* Copyright 2010-2021 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.incremental
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import javax.tools.ToolProvider
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class JavaClassNameTest {
@get:Rule
val tmpDir = TemporaryFolder()
@Test
fun `test compute JavaClassName`() {
val compiledClasses = compileJava(className, sourceCode)
val classNames = compiledClasses.map { JavaClassName.compute(it) }
assertEquals(
listOf(
"TopLevelClass: com/example/JavaClassWithNestedClasses",
"LocalClass: com/example/JavaClassWithNestedClasses\$1",
"LocalClass: com/example/JavaClassWithNestedClasses\$1LocalClass",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$1LocalClass\$InnerClassWithinLocalClass",
"LocalClass: com/example/JavaClassWithNestedClasses\$2",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$InnerClass",
"LocalClass: com/example/JavaClassWithNestedClasses\$InnerClass\$1LocalClassWithinInnerClass",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$InnerClass\$InnerClassWithinInnerClass",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$InnerClassWith\$Sign",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$StaticNestedClass"
),
classNames.map { "${it.javaClass.simpleName}: ${it.name}" }
)
}
@Test
fun `test computeJavaClassIds`() {
val compiledClasses = compileJava(className, sourceCode)
val classNames = compiledClasses.map { JavaClassName.compute(it) }
val classIds = computeJavaClassIds(classNames)
assertEquals(
listOf(
classId("com.example", "JavaClassWithNestedClasses", local = false),
classId("com.example", "JavaClassWithNestedClasses$1", local = true),
classId("com.example", "JavaClassWithNestedClasses$1LocalClass", local = true),
classId("com.example", "JavaClassWithNestedClasses$1LocalClass.InnerClassWithinLocalClass", local = true),
classId("com.example", "JavaClassWithNestedClasses$2", local = true),
classId("com.example", "JavaClassWithNestedClasses.InnerClass", local = false),
classId("com.example", "JavaClassWithNestedClasses\$InnerClass\$1LocalClassWithinInnerClass", local = true),
classId("com.example", "JavaClassWithNestedClasses.InnerClass.InnerClassWithinInnerClass", local = false),
classId("com.example", "JavaClassWithNestedClasses.InnerClassWith\$Sign", local = false),
classId("com.example", "JavaClassWithNestedClasses.StaticNestedClass", local = false)
),
classIds
)
}
@Suppress("SameParameterValue")
private fun compileJava(className: String, sourceCode: String): List<ByteArray> {
val sourceFile = File(tmpDir.newFolder(), "$className.java").apply {
parentFile.mkdirs()
writeText(sourceCode)
}
val classesDir = tmpDir.newFolder()
val compiler = ToolProvider.getSystemJavaCompiler()
compiler.getStandardFileManager(null, null, null).use { fileManager ->
val compilationTask = compiler.getTask(
null, fileManager, null,
listOf("-d", classesDir.path), null,
fileManager.getJavaFileObjectsFromFiles(listOf(sourceFile))
)
assertTrue(compilationTask.call(), "Failed to compile '$className'")
}
return classesDir.walk().filter { it.isFile }
.sortedBy { it.path.substringBefore(".class") }
.map { it.readBytes() }
.toList()
}
private fun classId(@Suppress("SameParameterValue") packageFqName: String, relativeClassName: String, local: Boolean) =
ClassId(FqName(packageFqName), FqName(relativeClassName), local)
}
private const val className = "com/example/JavaClassWithNestedClasses"
private val sourceCode = """
package com.example;
public class JavaClassWithNestedClasses {
public class InnerClass {
public void publicMethod() {
System.out.println("I'm in a public method");
}
private void privateMethod() {
System.out.println("I'm in a private method");
}
public class InnerClassWithinInnerClass {
}
public void someMethod() {
class LocalClassWithinInnerClass {
}
}
}
public static class StaticNestedClass {
}
public void someMethod() {
class LocalClass {
class InnerClassWithinLocalClass {
}
}
Runnable objectOfAnonymousLocalClass = new Runnable() {
@Override
public void run() {
}
};
}
private Runnable objectOfAnonymousNonLocalClass = new Runnable() {
@Override
public void run() {
}
};
public class InnerClassWith${'$'}Sign {
}
}
""".trimIndent()