diff --git a/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt b/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt index f423de274c9..2bba8bd0858 100644 --- a/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt +++ b/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt @@ -25,7 +25,6 @@ import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.build.GeneratedJvmClass import org.jetbrains.kotlin.incremental.storage.* import org.jetbrains.kotlin.inline.inlineFunctionsJvmNames -import org.jetbrains.kotlin.load.kotlin.FileBasedKotlinClass import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCache import org.jetbrains.kotlin.load.kotlin.incremental.components.JvmPackagePartProto @@ -630,30 +629,19 @@ class KotlinClassInfo constructor( companion object { fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo { - return KotlinClassInfo( - kotlinClass.classId, - kotlinClass.classHeader.kind, - kotlinClass.classHeader.data ?: emptyArray(), - kotlinClass.classHeader.strings ?: emptyArray(), - kotlinClass.classHeader.multifileClassName, - getConstantsMap(kotlinClass.fileContents), - getInlineFunctionsMap(kotlinClass.classHeader, kotlinClass.fileContents) - ) + return createFrom(kotlinClass.classId, kotlinClass.classHeader, kotlinClass.fileContents) } - /** Creates [KotlinClassInfo] from the given classContents, or returns `null` if the class is not a Kotlin class. */ - fun tryCreateFrom(classContents: ByteArray): KotlinClassInfo? { - return FileBasedKotlinClass.create(classContents) { classId, _, classHeader, _ -> - KotlinClassInfo( - classId, - classHeader.kind, - classHeader.data ?: emptyArray(), - classHeader.strings ?: emptyArray(), - classHeader.multifileClassName, - getConstantsMap(classContents), - getInlineFunctionsMap(classHeader, classContents) - ) - } + fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo { + return KotlinClassInfo( + classId, + classHeader.kind, + classHeader.data ?: emptyArray(), + classHeader.strings ?: emptyArray(), + classHeader.multifileClassName, + getConstantsMap(classContents), + getInlineFunctionsMap(classHeader, classContents) + ) } } } diff --git a/build-common/src/org/jetbrains/kotlin/incremental/JavaClassName.kt b/build-common/src/org/jetbrains/kotlin/incremental/JavaClassName.kt deleted file mode 100644 index 4aefd969e04..00000000000 --- a/build-common/src/org/jetbrains/kotlin/incremental/JavaClassName.kt +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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"). */ - abstract val name: 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 (e.g., "com/example/Foo$1"). - */ - abstract val isAnonymous: Boolean - - /** Whether this class is a synthetic class. */ - abstract val isSynthetic: Boolean - - /** The package name of this class (e.g., "com/example"). */ - val packageName: String - get() = name.substringBeforeLast('/', missingDelimiterValue = "") - - /** The part of the full name of this class after [packageName] (e.g., "Foo$Bar"). */ - val relativeClassName: String - get() = name.substringAfterLast('/', missingDelimiterValue = name) - - companion object { - - /** Computes the [JavaClassName] of a compiled Java class given its contents. */ - fun compute(classContents: ByteArray): JavaClassName { - val nameRef = Ref.create() - val isSyntheticRef = Ref.create() - val isTopLevelRef = Ref.create() - val outerNameRef = Ref.create() - val isAnonymousRef = Ref.create() - - ClassReader(classContents).accept(object : ClassVisitor(Opcodes.API_VERSION) { - override fun visit( - version: Int, access: Int, name: String, - signature: String?, superName: String?, interfaces: Array? - ) { - nameRef.set(name) - isSyntheticRef.set((access and Opcodes.ACC_SYNTHETIC) != 0) - } - - override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) { - if (name == nameRef.get()!!) { - isTopLevelRef.set(false) - outerNameRef.set(outerName) - isAnonymousRef.set(innerName == null) - } - } - }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) - - val name = nameRef.get()!! - val isSynthetic = isSyntheticRef.get()!! - val isTopLevel = isTopLevelRef.get() ?: true - val outerName = outerNameRef.get() - val isAnonymous = isAnonymousRef.get() - - return when { - isTopLevel -> TopLevelClass(name, isSynthetic) - outerName != null -> NestedNonLocalClass(name, outerName, isAnonymous!!, isSynthetic) - else -> LocalClass(name, isAnonymous!!, isSynthetic) - } - } - } -} - -/** See [JavaClassName]. */ -class TopLevelClass( - override val name: String, - override val isSynthetic: Boolean -) : JavaClassName() { - override val isAnonymous: Boolean = false // A top-level class is never anonymous -} - -/** See [JavaClassName]. */ -sealed class NestedClass : JavaClassName() - -/** See [JavaClassName]. */ -class NestedNonLocalClass( - override val 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 "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, - - override val isAnonymous: Boolean, - override val isSynthetic: Boolean -) : NestedClass() { - - /** - * 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 [JavaClassName.isAnonymous]). - */ - val simpleName: String - get() = run { - check(name.startsWith("$outerName\$")) - name.substring("$outerName\$".length).also { check(it.isNotEmpty()) } - } -} - -/** See [JavaClassName]. */ -class LocalClass( - override val name: String, - override val isAnonymous: Boolean, - override val isSynthetic: Boolean -) : NestedClass() - -/** - * 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 (outer) 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(classNames: List): List { - val classNameToClassId: MutableMap = HashMap(classNames.size) - val nameToClassName: Map = classNames.associateBy { it.name } - - fun JavaClassName.getClassId(): ClassId { - classNameToClassId[this]?.let { return it } - - val packageName = FqName(packageName.replace('/', '.')) - val classId = when (this) { - is TopLevelClass -> { - ClassId(packageName, FqName(relativeClassName), /* local */ false) - } - is NestedNonLocalClass -> { - // JavaClassName.relativeClassName can contain '$' but not '.', whereas ClassId.relativeClassName can contain both '$' and - // '.' (e.g., "com/example/OuterClassWith$Sign$NestedClassWith$Sign" has JavaClassName.relativeClassName - // "OuterClassWith$Sign$NestedClassWith$Sign", but its ClassId.relativeClassName will be - // "OuterClassWith$Sign.NestedClassWith$Sign". To disambiguate '$' in the (outer) class name, we need to get the ClassId of - // the outer class first. - val outerClassId = nameToClassName[outerName]?.getClassId() ?: error("Can't find outer class '$outerName' of class '$name'") - val relativeClassName = FqName(outerClassId.relativeClassName.asString() + "." + 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 source 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. - ClassId(packageName, FqName(relativeClassName), /* local */ true) - } - } - - return classId.also { - classNameToClassId[this] = it - } - } - - return classNames.map { it.getClassId() } -} diff --git a/build-common/test/org/jetbrains/kotlin/incremental/JavaClassNameTest.kt b/build-common/test/org/jetbrains/kotlin/incremental/JavaClassNameTest.kt deleted file mode 100644 index eaaf1d7bd54..00000000000 --- a/build-common/test/org/jetbrains/kotlin/incremental/JavaClassNameTest.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.jetbrains.kotlin.test.KotlinTestUtils -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import java.io.File -import kotlin.test.assertEquals - -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 { - val sourceFile = File(tmpDir.newFolder(), "$className.java").apply { - parentFile.mkdirs() - writeText(sourceCode) - } - val classesDir = tmpDir.newFolder() - - KotlinTestUtils.compileJavaFiles(listOf(sourceFile), listOf("-d", classesDir.path)) - - 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() diff --git a/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/FileBasedKotlinClass.java b/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/FileBasedKotlinClass.java index 0700386b2ba..75d1ae89a3e 100644 --- a/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/FileBasedKotlinClass.java +++ b/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/FileBasedKotlinClass.java @@ -320,14 +320,14 @@ public abstract class FileBasedKotlinClass implements KotlinJvmBinaryClass { } @NotNull - private static ClassId resolveNameByInternalName(@NotNull String name, @NotNull InnerClassesInfo innerClasses) { + public static ClassId resolveNameByInternalName(@NotNull String name, @NotNull InnerClassesInfo innerClasses) { if (!name.contains("$")) { return ClassId.topLevel(new FqName(name.replace('/', '.'))); } List classes = new ArrayList<>(1); boolean local = false; - + while (true) { OuterAndInnerName outer = innerClasses.get(name); if (outer == null) break; diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/BasicClassInfo.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/BasicClassInfo.kt new file mode 100644 index 00000000000..4a17cabef37 --- /dev/null +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/BasicClassInfo.kt @@ -0,0 +1,115 @@ +/* + * 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.classpathDiff + +import org.jetbrains.kotlin.load.kotlin.FileBasedKotlinClass.* +import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader +import org.jetbrains.kotlin.load.kotlin.header.ReadKotlinClassHeaderAnnotationVisitor +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.resolve.jvm.JvmClassName +import org.jetbrains.org.objectweb.asm.AnnotationVisitor +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.ClassVisitor +import org.jetbrains.org.objectweb.asm.Opcodes + +/** Basic information about a class (e.g., [classId], [kotlinClassHeader] and [supertypes]). */ +class BasicClassInfo( + val classId: ClassId, + val kotlinClassHeader: KotlinClassHeader?, // null if this is not a Kotlin class + val supertypes: List, + + private val accessFlags: Int, + val isAnonymous: Boolean +) { + val isKotlinClass = kotlinClassHeader != null + val isPrivate = flagEnabled(accessFlags, Opcodes.ACC_PRIVATE) + val isLocal = classId.isLocal + + /** Whether this is a synthetic Java class. */ + val isJavaSynthetic = !isKotlinClass && flagEnabled(accessFlags, Opcodes.ACC_SYNTHETIC) + + /** + * Whether this is a synthetic Kotlin class. + * + * Note that we use [KotlinClassHeader.Kind], not [accessFlags], to make this determination. For example, + * 'kotlin/Metadata.DefaultImpls' is synthetic according to its [KotlinClassHeader.Kind], but not synthetic according to its + * [accessFlags]. + */ + @Suppress("unused") + val isKotlinSynthetic = isKotlinClass && kotlinClassHeader!!.kind == KotlinClassHeader.Kind.SYNTHETIC_CLASS + + private fun flagEnabled(accessFlags: Int, flagToCheck: Int) = (accessFlags and flagToCheck) != 0 + + companion object { + + fun compute(classContents: ByteArray): BasicClassInfo { + val kotlinClassHeaderClassVisitor = KotlinClassHeaderClassVisitor() + val innerClassesClassVisitor = InnerClassesClassVisitor(kotlinClassHeaderClassVisitor) + val basicClassInfoVisitor = BasicClassInfoClassVisitor(innerClassesClassVisitor) + + ClassReader(classContents).accept( + basicClassInfoVisitor, + ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES + ) + + val className = basicClassInfoVisitor.getClassName() + val innerClassesInfo = innerClassesClassVisitor.getInnerClassesInfo() + + return BasicClassInfo( + classId = resolveNameByInternalName(className, innerClassesInfo), + kotlinClassHeader = kotlinClassHeaderClassVisitor.getKotlinClassHeader(), + supertypes = basicClassInfoVisitor.getSupertypes(), + accessFlags = basicClassInfoVisitor.getAccessFlags(), + isAnonymous = innerClassesInfo[className]?.let { it.innerSimpleName == null } ?: false + ) + } + } +} + +private class BasicClassInfoClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.API_VERSION, cv) { + private var className: String? = null + private var classAccess: Int? = null + private val supertypeNames = mutableListOf() + + override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array?) { + className = name + classAccess = access + superName?.let { supertypeNames.add(it) } + interfaces?.let { supertypeNames.addAll(it) } + super.visit(version, access, name, signature, superName, interfaces) + } + + fun getClassName(): String = className!! + fun getAccessFlags(): Int = classAccess!! + fun getSupertypes(): List = supertypeNames.map { JvmClassName.byInternalName(it) } +} + +private class InnerClassesClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.API_VERSION, cv) { + + private val innerClassesInfo = InnerClassesInfo() + + override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) { + innerClassesInfo.add(name, outerName, innerName) + super.visitInnerClass(name, outerName, innerName, access) + } + + fun getInnerClassesInfo(): InnerClassesInfo = innerClassesInfo +} + +private class KotlinClassHeaderClassVisitor : ClassVisitor(Opcodes.API_VERSION) { + + private val kotlinClassHeaderAnnotationVisitor = ReadKotlinClassHeaderAnnotationVisitor() + + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + return convertAnnotationVisitor( + kotlinClassHeaderAnnotationVisitor, + descriptor, + InnerClassesInfo() // This info is not needed to resolve KotlinClassHeader + ) + } + + fun getKotlinClassHeader(): KotlinClassHeader? = kotlinClassHeaderAnnotationVisitor.createHeader() +} diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt index 01552b83689..453bbee2d6c 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt @@ -153,7 +153,7 @@ object ClasspathChangesComputer { ) incrementalJvmCache.markDirty(JvmClassName.byClassId(previousSnapshot.serializedJavaClass.classId)) } - is RegularJavaClassSnapshot, is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> { + is RegularJavaClassSnapshot, is InaccessibleClassSnapshot, is ContentHashJavaClassSnapshot -> { error("Unexpected type (it should have been handled earlier): ${previousSnapshot.javaClass.name}") } } @@ -183,7 +183,7 @@ object ClasspathChangesComputer { collector = changesCollector ) } - is RegularJavaClassSnapshot, is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> { + is RegularJavaClassSnapshot, is InaccessibleClassSnapshot, is ContentHashJavaClassSnapshot -> { error("Unexpected type (it should have been handled earlier): ${currentSnapshot.javaClass.name}") } } @@ -331,7 +331,7 @@ internal fun ClassSnapshot.getClassId(): ClassId { is KotlinClassSnapshot -> classInfo.classId is RegularJavaClassSnapshot -> classId is ProtoBasedJavaClassSnapshot -> serializedJavaClass.classId - is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> { + is InaccessibleClassSnapshot, is ContentHashJavaClassSnapshot -> { error("Unexpected type (it should have been handled earlier): ${javaClass.name}") } } @@ -351,7 +351,7 @@ internal fun ClassSnapshot.getSupertypes(classIdResolver: (JvmClassName) -> Clas val (proto, nameResolver) = serializedJavaClass.toProtoData() proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) } } - is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> { + is InaccessibleClassSnapshot, is ContentHashJavaClassSnapshot -> { error("Unexpected type (it should have been handled earlier): ${javaClass.name}") } } diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshot.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshot.kt index ee019a0609a..c15b7cb2e59 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshot.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshot.kt @@ -107,12 +107,12 @@ class AbiSnapshotForTests( class ProtoBasedJavaClassSnapshot(val serializedJavaClass: SerializedJavaClass) : JavaClassSnapshot() /** - * [JavaClassSnapshot] of a Java class where there is nothing to capture. + * [ClassSnapshot] of an inaccessible class. * - * For example, the snapshot of a local class is empty as a local class can't be referenced from other source files and therefore any - * changes in a local class will not cause recompilation of other source files. + * For example, a local class is inaccessible as it can't be referenced from other source files and therefore any changes in a local class + * will not require recompilation of other source files. */ -object EmptyJavaClassSnapshot : JavaClassSnapshot() +object InaccessibleClassSnapshot : ClassSnapshot() /** * [JavaClassSnapshot] of a Java class where a proper snapshot can't be created for some reason, so we use the hash of the class contents as diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt index b8096afc78e..a70cbd4c0a9 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt @@ -65,19 +65,28 @@ object ClasspathEntrySnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, snapshot: ClassSnapshot) { - output.writeBoolean(snapshot is KotlinClassSnapshot) when (snapshot) { - is KotlinClassSnapshot -> KotlinClassSnapshotExternalizer.save(output, snapshot) - is JavaClassSnapshot -> JavaClassSnapshotExternalizer.save(output, snapshot) + is KotlinClassSnapshot -> { + output.writeString(KotlinClassSnapshot::class.java.name) + KotlinClassSnapshotExternalizer.save(output, snapshot) + } + is JavaClassSnapshot -> { + output.writeString(JavaClassSnapshot::class.java.name) + JavaClassSnapshotExternalizer.save(output, snapshot) + } + is InaccessibleClassSnapshot -> { + output.writeString(InaccessibleClassSnapshot::class.java.name) + InaccessibleClassSnapshotExternalizer.save(output, snapshot) + } } } override fun read(input: DataInput): ClassSnapshot { - val isKotlinClassSnapshot = input.readBoolean() - return if (isKotlinClassSnapshot) { - KotlinClassSnapshotExternalizer.read(input) - } else { - JavaClassSnapshotExternalizer.read(input) + return when (val className = input.readString()) { + KotlinClassSnapshot::class.java.name -> KotlinClassSnapshotExternalizer.read(input) + JavaClassSnapshot::class.java.name -> JavaClassSnapshotExternalizer.read(input) + InaccessibleClassSnapshot::class.java.name -> InaccessibleClassSnapshotExternalizer.read(input) + else -> error("Unrecognized class name: $className") } } } @@ -144,7 +153,6 @@ object JavaClassSnapshotExternalizer : DataExternalizer { when (snapshot) { is RegularJavaClassSnapshot -> RegularJavaClassSnapshotExternalizer.save(output, snapshot) is ProtoBasedJavaClassSnapshot -> ProtoBasedJavaClassSnapshotExternalizer.save(output, snapshot) - is EmptyJavaClassSnapshot -> EmptyJavaClassSnapshotExternalizer.save(output, snapshot) is ContentHashJavaClassSnapshot -> ContentHashJavaClassSnapshotExternalizer.save(output, snapshot) } } @@ -153,7 +161,6 @@ object JavaClassSnapshotExternalizer : DataExternalizer { return when (val className = input.readString()) { RegularJavaClassSnapshot::class.java.name -> RegularJavaClassSnapshotExternalizer.read(input) ProtoBasedJavaClassSnapshot::class.java.name -> ProtoBasedJavaClassSnapshotExternalizer.read(input) - EmptyJavaClassSnapshot::class.java.name -> EmptyJavaClassSnapshotExternalizer.read(input) ContentHashJavaClassSnapshot::class.java.name -> ContentHashJavaClassSnapshotExternalizer.read(input) else -> error("Unrecognized class name: $className") } @@ -204,14 +211,14 @@ object ProtoBasedJavaClassSnapshotExternalizer : DataExternalizer { +object InaccessibleClassSnapshotExternalizer : DataExternalizer { - override fun save(output: DataOutput, snapshot: EmptyJavaClassSnapshot) { + override fun save(output: DataOutput, snapshot: InaccessibleClassSnapshot) { // Nothing to save } - override fun read(input: DataInput): EmptyJavaClassSnapshot { - return EmptyJavaClassSnapshot + override fun read(input: DataInput): InaccessibleClassSnapshot { + return InaccessibleClassSnapshot } } diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotShrinker.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotShrinker.kt index eaf17f1f30f..2c1ce8345f3 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotShrinker.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotShrinker.kt @@ -20,7 +20,7 @@ object ClasspathSnapshotShrinker { /** * Shrinks the given [ClasspathSnapshot] by retaining only classes that are referenced. Referencing info is stored in [LookupStorage]. * - * This method also removes duplicate classes and [EmptyJavaClassSnapshot]s first. + * This method also removes duplicate classes and [InaccessibleClassSnapshot]s first. */ fun shrink( classpathSnapshot: ClasspathSnapshot, @@ -28,10 +28,10 @@ object ClasspathSnapshotShrinker { metrics: BuildMetricsReporter ): List { val allClasses = metrics.measure(BuildTime.GET_NON_DUPLICATE_CLASSES) { - // It's important to remove duplicate classes first before removing `EmptyJavaClassSnapshot`s. + // It's important to remove duplicate classes first before removing `InaccessibleClassSnapshot`s. // For example, if jar1!/com/example/A.class is empty and jar2!/com/example/A.class is non-empty, incorrect order of the actions // will lead to incorrect results. - classpathSnapshot.getNonDuplicateClassSnapshots().filter { it.classSnapshot !is EmptyJavaClassSnapshot } + classpathSnapshot.getNonDuplicateClassSnapshots().filter { it.classSnapshot !is InaccessibleClassSnapshot } } val lookupSymbols = metrics.measure(BuildTime.GET_LOOKUP_SYMBOLS) { lookupStorage.lookupMap.keys diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotter.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotter.kt index ad6b577155b..402b5156919 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotter.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotter.kt @@ -5,18 +5,14 @@ package org.jetbrains.kotlin.incremental.classpathDiff -import org.jetbrains.kotlin.incremental.* +import org.jetbrains.kotlin.incremental.JavaClassDescriptorCreator +import org.jetbrains.kotlin.incremental.KotlinClassInfo +import org.jetbrains.kotlin.incremental.md5 +import org.jetbrains.kotlin.incremental.toSerializedJavaClass import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor -import org.jetbrains.kotlin.metadata.deserialization.TypeTable -import org.jetbrains.kotlin.metadata.deserialization.supertypes -import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil +import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.resolve.jvm.JvmClassName -import org.jetbrains.kotlin.serialization.deserialization.getClassId import org.jetbrains.kotlin.utils.DFS -import org.jetbrains.org.objectweb.asm.ClassReader -import org.jetbrains.org.objectweb.asm.ClassVisitor -import org.jetbrains.org.objectweb.asm.Opcodes import java.io.File import java.util.zip.ZipInputStream @@ -40,7 +36,7 @@ object ClasspathEntrySnapshotter { val snapshots = try { ClassSnapshotter.snapshot(classes, protoBased).map { it.addHash() } } catch (e: Throwable) { - if (isKnownProblematicClasspathEntry(classpathEntry)) { + if ((protoBased ?: protoBasedDefaultValue) && isKnownProblematicClasspathEntry(classpathEntry)) { classes.map { ContentHashJavaClassSnapshot(it.contents.md5()).addHash() } } else throw e } @@ -74,112 +70,83 @@ object ClasspathEntrySnapshotter { /** Creates [ClassSnapshot]s of classes. */ object ClassSnapshotter { - /** - * Creates [ClassSnapshot]s of the given classes. - * - * Note that for Java (non-Kotlin) classes, creating a [ClassSnapshot] 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. - */ + /** Creates [ClassSnapshot]s of the given classes. */ fun snapshot( classes: List, protoBased: Boolean? = null, includeDebugInfoInSnapshot: Boolean? = null ): List { - // Snapshot Kotlin classes first - val kotlinClassSnapshots: Map = classes.associateWith { - trySnapshotKotlinClass(it) - } + val classesInfo: List = classes.map { BasicClassInfo.compute(it.contents) } - // Snapshot the remaining Java classes in one invocation + // Find inaccessible classes first, their snapshots will be `InaccessibleClassSnapshot`s. + val inaccessibleClasses: Set = getInaccessibleClasses(classesInfo).toSet() + + // Snapshot the remaining accessible classes + val accessibleClasses: List = classes.mapIndexedNotNull { index, clazz -> + if (classesInfo[index] in inaccessibleClasses) null else clazz + } + val accessibleClassesInfo: List = classesInfo.filterNot { it in inaccessibleClasses } + val accessibleSnapshots: List = + doSnapshot(accessibleClasses, accessibleClassesInfo, protoBased, includeDebugInfoInSnapshot) + val accessibleClassSnapshots: Map = accessibleClasses.zipToMap(accessibleSnapshots) + + return classes.map { accessibleClassSnapshots[it] ?: InaccessibleClassSnapshot } + } + + private fun doSnapshot( + classes: List, + classesInfo: List, + protoBased: Boolean? = null, + includeDebugInfoInSnapshot: Boolean? = null + ): List { + // Snapshot Kotlin classes first + val kotlinSnapshots: List = classes.mapIndexed { index, clazz -> + trySnapshotKotlinClass(clazz, classesInfo[index]) + } + val kotlinClassSnapshots: Map = classes.zipToMap(kotlinSnapshots) + + // Snapshot the remaining Java classes val javaClasses: List = classes.filter { kotlinClassSnapshots[it] == null } - val snapshots: List = snapshotJavaClasses(javaClasses, protoBased, includeDebugInfoInSnapshot) - val javaClassSnapshots: Map = javaClasses.zipToMap(snapshots) + val javaClassesInfo: List = classesInfo.mapIndexedNotNull { index, classInfo -> + val javaClass = classes[index] + if (kotlinClassSnapshots[javaClass] == null) classInfo else null + } + val javaSnapshots: List = + snapshotJavaClasses(javaClasses, javaClassesInfo, protoBased, includeDebugInfoInSnapshot) + val javaClassSnapshots: Map = javaClasses.zipToMap(javaSnapshots) return classes.map { kotlinClassSnapshots[it] ?: javaClassSnapshots[it]!! } } /** Creates [KotlinClassSnapshot] of the given class, or returns `null` if the class is not a Kotlin class. */ - private fun trySnapshotKotlinClass(clazz: ClassFileWithContents): KotlinClassSnapshot? { - return KotlinClassInfo.tryCreateFrom(clazz.contents)?.let { - KotlinClassSnapshot(it, computeSupertypes(it, clazz.contents)) - } + private fun trySnapshotKotlinClass(classFile: ClassFileWithContents, classInfo: BasicClassInfo): KotlinClassSnapshot? { + return if (classInfo.isKotlinClass) { + val kotlinClassInfo = KotlinClassInfo.createFrom(classInfo.classId, classInfo.kotlinClassHeader!!, classFile.contents) + KotlinClassSnapshot(kotlinClassInfo, classInfo.supertypes) + } else null } - // TODO: Find a faster way to get supertypes without loading protos (e.g., attach to an existing ASM visitor) - private fun computeSupertypes(classInfo: KotlinClassInfo, classContents: ByteArray): List { - return try { - val (nameResolver, proto) = JvmProtoBufUtil.readClassDataFrom(classInfo.classHeaderData, classInfo.classHeaderStrings) - val supertypeClassIds = proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) } - supertypeClassIds.map { JvmClassName.byClassId(it) } - } catch (e: Exception) { - // The above method call currently fails on a few classes for some reason: - // - org.jetbrains.kotlin.protobuf.InvalidProtocolBufferException: Message was missing required fields. - // (Lite runtime could not determine which fields were missing) for SomeClassKt.class - // - java.lang.NullPointerException: parseDelimitedFrom(this, EXTENSION_REGISTRY) must not be null for - // kotlin-stdlib-1.6.255-SNAPSHOT.jar - // Fall back to ASM visitor to get the supertypes. - val supertypeClassNames = mutableListOf() - ClassReader(classContents).accept(object : ClassVisitor(Opcodes.API_VERSION) { - override fun visit( - version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array? - ) { - superName?.let { supertypeClassNames.add(it) } - interfaces?.let { supertypeClassNames.addAll(it) } - } - }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG) - supertypeClassNames.map { JvmClassName.byInternalName(it) } - } - } - - /** - * Creates [JavaClassSnapshot]s of the given Java classes. - * - * Note that creating a [JavaClassSnapshot] 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. - */ + /** Creates [JavaClassSnapshot]s of the given Java classes. */ private fun snapshotJavaClasses( classes: List, + classesInfo: List, protoBased: Boolean? = null, includeDebugInfoInSnapshot: Boolean? = null ): List { - val classNames: List = classes.map { JavaClassName.compute(it.contents) } - val classNameToClassFile: Map = classNames.zipToMap(classes) - - // We divide classes into 2 categories: - // - Special classes, which includes local, anonymous, or synthetic classes, and their nested classes. These classes can't be - // referenced from other source files, so any changes in these classes will not cause recompilation of other source files. - // Therefore, the snapshots of these classes are empty. - // - Regular classes: Any classes that do not belong to the above category. - val specialClasses = getSpecialClasses(classNames).toSet() - - // Snapshot special classes first - val specialClassSnapshots: Map = classNames.associateWith { - if (it in specialClasses) { - EmptyJavaClassSnapshot - } else null - } - - // Snapshot the remaining regular classes - val regularClasses: List = classNames.filter { specialClassSnapshots[it] == null } - val regularClassIds = computeJavaClassIds(regularClasses) - val regularClassFiles: List = regularClasses.map { classNameToClassFile[it]!! } - - val snapshots: List = if (protoBased ?: protoBasedDefaultValue) { - snapshotJavaClassesProtoBased(regularClassIds, regularClassFiles) + return if (protoBased ?: protoBasedDefaultValue) { + snapshotJavaClassesProtoBased(classes, classesInfo) } else { - regularClassIds.mapIndexed { index, classId -> - JavaClassSnapshotter.snapshot(classId, regularClassFiles[index].contents, includeDebugInfoInSnapshot) + classes.mapIndexed { index, clazz -> + JavaClassSnapshotter.snapshot(clazz.contents, classesInfo[index], includeDebugInfoInSnapshot) } } - val regularClassSnapshots: Map = regularClasses.zipToMap(snapshots) - - return classNames.map { specialClassSnapshots[it] ?: regularClassSnapshots[it]!! } } private fun snapshotJavaClassesProtoBased( - classIds: List, - classFilesWithContents: List + classFilesWithContents: List, + classesInfo: List ): List { + val classIds = classesInfo.map { it.classId } val classesContents = classFilesWithContents.map { it.contents } val descriptors: List = JavaClassDescriptorCreator.create(classIds, classesContents) val snapshots: List = descriptors.mapIndexed { index, descriptor -> @@ -206,28 +173,52 @@ object ClassSnapshotter { return snapshots } - /** Returns local, anonymous, or synthetic classes, and their nested classes. */ - private fun getSpecialClasses(classNames: List): List { - val specialClasses: MutableMap = HashMap(classNames.size) - val nameToClassName: Map = classNames.associateBy { it.name } - - fun JavaClassName.isSpecial(): Boolean { - specialClasses[this]?.let { return it } - - return if (isAnonymous || isSynthetic) { - true - } else when (this) { - is TopLevelClass -> false - is NestedNonLocalClass -> { - nameToClassName[outerName]?.isSpecial() ?: error("Can't find outer class '$outerName' of class '$name'") - } - is LocalClass -> true - }.also { - specialClasses[this] = it + /** + * Returns inaccessible classes, i.e. classes that can't be referenced from other source files (and therefore any changes in these + * classes will not require recompilation of other source files). + * + * Examples include private, local, anonymous, and synthetic classes. + * + * If a class is inaccessible, its nested classes (if any) are also inaccessible. + * + * NOTE: If we do not have enough info to determine whether a class is inaccessible, we will assume that the class is accessible to be + * safe. + */ + private fun getInaccessibleClasses(classesInfo: List): List { + fun BasicClassInfo.isInaccessible(): Boolean { + if (this.isKotlinClass && this.kotlinClassHeader!!.kind != KotlinClassHeader.Kind.CLASS) { + // We're not sure about these kinds of Kotlin classes, so we assume it's accessible (see this method's kdoc) + return false + } + return if (isKotlinClass) { + // TODO: Is it safe to add isKotlinSynthetic to this lists? + isPrivate || isLocal || isAnonymous + } else { + isPrivate || isLocal || isAnonymous || isJavaSynthetic } } - return classNames.filter { it.isSpecial() } + val classIsInaccessible: MutableMap = HashMap(classesInfo.size) + val classIdToClassInfo: Map = classesInfo.associateBy { it.classId } + + fun BasicClassInfo.isTransitivelyInaccessible(): Boolean { + classIsInaccessible[this]?.let { return it } + + val inaccessible = if (isInaccessible()) { + true + } else classId.outerClassId?.let { outerClassId -> + classIdToClassInfo[outerClassId]?.isTransitivelyInaccessible() + // If we can't find the outer class from the given list of classes (which could happen with faulty jars), we assume that + // the class is accessible (see this method's kdoc). + ?: false + } ?: false + + return inaccessible.also { + classIsInaccessible[this] = inaccessible + } + } + + return classesInfo.filter { it.isTransitivelyInaccessible() } } /** Returns `true` if it is known that the given exception can be thrown when calling [JavaClassDescriptor.toSerializedJavaClass]. */ diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/JavaClassSnapshotter.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/JavaClassSnapshotter.kt index 06e57eb6557..f321e0726ad 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/JavaClassSnapshotter.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/JavaClassSnapshotter.kt @@ -7,8 +7,6 @@ package org.jetbrains.kotlin.incremental.classpathDiff import com.google.gson.GsonBuilder import org.jetbrains.kotlin.incremental.md5 -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.org.objectweb.asm.ClassReader import org.jetbrains.org.objectweb.asm.Opcodes import org.jetbrains.org.objectweb.asm.tree.ClassNode @@ -16,7 +14,7 @@ import org.jetbrains.org.objectweb.asm.tree.ClassNode /** Computes a [JavaClassSnapshot] of a Java class. */ object JavaClassSnapshotter { - fun snapshot(classId: ClassId, classContents: ByteArray, includeDebugInfoInSnapshot: Boolean? = null): JavaClassSnapshot { + fun snapshot(classContents: ByteArray, classInfo: BasicClassInfo, includeDebugInfoInSnapshot: Boolean? = null): JavaClassSnapshot { // We will extract ABI information from the given class and store it into the `abiClass` variable. // It is acceptable to collect more info than required, but it is incorrect to collect less info than required. // There are 2 approaches: @@ -28,28 +26,25 @@ object JavaClassSnapshotter { // First, collect all info. // Note the parsing options passed to ClassReader: - // - SKIP_CODE is set as method bodies will not be part of the ABI of the class. + // - SKIP_CODE and SKIP_FRAMES are set as method bodies will not be part of the ABI of the class. // - SKIP_DEBUG is not set as it would skip method parameters, which may be used by annotation processors like Room. - // - SKIP_FRAMES and EXPAND_FRAMES are not relevant when SKIP_CODE is set. + // - EXPAND_FRAMES is not needed (and not relevant when SKIP_CODE is set). val classReader = ClassReader(classContents) - classReader.accept(abiClass, ClassReader.SKIP_CODE) + classReader.accept(abiClass, ClassReader.SKIP_CODE or ClassReader.SKIP_FRAMES) - // Then, remove non-ABI info: - // - Method bodies have already been removed (see SKIP_CODE above). - // - If the class is private, its snapshot will be empty. Otherwise, remove its private fields and methods. - if (abiClass.access.isPrivate()) { - return EmptyJavaClassSnapshot - } + // Then, remove non-ABI info, which includes: + // - Method bodies: Should have already been removed (see SKIP_CODE above) + // - Private fields and methods + fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0 abiClass.fields.removeIf { it.access.isPrivate() } abiClass.methods.removeIf { it.access.isPrivate() } - // Sort fields and methods as their order is not important (we still use List instead of Set as we want the serialized snapshot to - // be deterministic). + // Normalize the class: Sort fields and methods as their order is not important (we still use List instead of Set as we want the + // serialized snapshot to be deterministic). abiClass.fields.sortWith(compareBy({ it.name }, { it.desc })) abiClass.methods.sortWith(compareBy({ it.name }, { it.desc })) - val supertypes = (listOf(abiClass.superName) + abiClass.interfaces.toList()).map { JvmClassName.byInternalName(it) } - + // Snapshot the class val fieldsAbi = abiClass.fields.map { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) } val methodsAbi = abiClass.methods.map { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) } @@ -57,30 +52,32 @@ object JavaClassSnapshotter { abiClass.methods.clear() val classAbiExcludingMembers = abiClass.let { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) } - return RegularJavaClassSnapshot(classId, supertypes, classAbiExcludingMembers, fieldsAbi, methodsAbi) + return RegularJavaClassSnapshot(classInfo.classId, classInfo.supertypes, classAbiExcludingMembers, fieldsAbi, methodsAbi) } - private fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0 - private val gson by lazy { - // Use serializeSpecialFloatingPointValues() to avoid + // Use serializeSpecialFloatingPointValues() to avoid this error // "java.lang.IllegalArgumentException: NaN is not a valid double value as per JSON specification. To override this behavior, use // GsonBuilder.serializeSpecialFloatingPointValues() method." // on jars such as ~/.gradle/kotlin-build-dependencies/repo/kotlin.build/ideaIC/203.8084.24/artifacts/lib/rhino-1.7.12.jar. - GsonBuilder() - .serializeSpecialFloatingPointValues() + GsonBuilder().serializeSpecialFloatingPointValues().create() + } + + // Same as above but with `setPrettyPrinting()` + private val gsonForDebug by lazy { + GsonBuilder().serializeSpecialFloatingPointValues() .setPrettyPrinting() .create() } private fun snapshotJavaElement(javaElement: Any, javaElementName: String, includeDebugInfoInSnapshot: Boolean? = null): AbiSnapshot { - // TODO: Optimize this method later if necessary. Currently we focus on correctness first. - val abiValue = gson.toJson(javaElement) - val abiHash = abiValue.toByteArray().md5() - return if (includeDebugInfoInSnapshot == true) { + val abiValue = gsonForDebug.toJson(javaElement) + val abiHash = abiValue.toByteArray().md5() AbiSnapshotForTests(javaElementName, abiHash, abiValue) } else { + val abiValue = gson.toJson(javaElement) + val abiHash = abiValue.toByteArray().md5() AbiSnapshot(javaElementName, abiHash) } } diff --git a/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/BasicClassInfoTest.kt b/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/BasicClassInfoTest.kt new file mode 100644 index 00000000000..7fff9d05d7a --- /dev/null +++ b/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/BasicClassInfoTest.kt @@ -0,0 +1,111 @@ +/* + * 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.classpathDiff + +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.test.KotlinTestUtils +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.assertEquals + +class BasicClassInfoTest { + + @get:Rule + val tmpDir = TemporaryFolder() + + @Test + fun `compute BasicClassInfo`() { + val compiledClasses = compileJava(className, sourceCode) + val classIds = compiledClasses.map { BasicClassInfo.compute(it).classId } + + assertEquals( + listOf( + classId("com.example", "TopLevelClass", local = false), + classId("com.example", "TopLevelClass$1", local = true), + classId("com.example", "TopLevelClass$1LocalClass", local = true), + classId("com.example", "TopLevelClass$1LocalClass$1LocalClassWithinLocalClass", local = true), + classId("com.example", "TopLevelClass$1LocalClass.InnerClassWithinLocalClass", local = true), + classId("com.example", "TopLevelClass$2", local = true), + classId("com.example", "TopLevelClass.InnerClass", local = false), + classId("com.example", "TopLevelClass\$InnerClass\$1LocalClassWithinInnerClass", local = true), + classId("com.example", "TopLevelClass.InnerClass.InnerClassWithinInnerClass", local = false), + classId("com.example", "TopLevelClass.InnerClassWith\$Sign", local = false), + classId("com.example", "TopLevelClass.InnerClassWith\$Sign.InnerClassWith\$SignLevel2", local = false), + classId("com.example", "TopLevelClass.StaticNestedClass", local = false) + ), + classIds + ) + } + + @Suppress("SameParameterValue") + private fun compileJava(className: String, sourceCode: String): List { + val sourceFile = File(tmpDir.newFolder(), "$className.java").apply { + parentFile.mkdirs() + writeText(sourceCode) + } + val classesDir = tmpDir.newFolder() + + KotlinTestUtils.compileJavaFiles(listOf(sourceFile), listOf("-d", classesDir.path)) + + return classesDir.walk().toList() + .filter { it.isFile } + .sortedBy { it.path.substringBefore(".class") } + .map { it.readBytes() } + } + + private fun classId(@Suppress("SameParameterValue") packageFqName: String, relativeClassName: String, local: Boolean) = + ClassId(FqName(packageFqName), FqName(relativeClassName), local) +} + +private const val className = "com/example/TopLevelClass" +private val sourceCode = """ +package com.example; + +public class TopLevelClass { + + public class InnerClass { + public class InnerClassWithinInnerClass { + } + public void someMethod() { + class LocalClassWithinInnerClass { + } + } + } + + public static class StaticNestedClass { + } + + public void methodWithinTopLevelClass() { + class LocalClass { + class InnerClassWithinLocalClass { + } + public void methodWithinLocalClass() { + class LocalClassWithinLocalClass { + } + } + } + Runnable anonymousLocalClassInstance = new Runnable() { + @Override + public void run() { + } + }; + } + + private Runnable anonymousNonLocalClassInstance = new Runnable() { + @Override + public void run() { + } + }; + + public class InnerClassWith${'$'}Sign { + public class InnerClassWith${'$'}SignLevel2 { + } + } +} +""".trimIndent() diff --git a/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotTestCommon/expected-snapshot/kotlin/com/example/SimpleKotlinClass.json b/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotTestCommon/expected-snapshot/kotlin/com/example/SimpleKotlinClass.json index 7a43b7358a2..4deed64ff55 100644 --- a/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotTestCommon/expected-snapshot/kotlin/com/example/SimpleKotlinClass.json +++ b/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotTestCommon/expected-snapshot/kotlin/com/example/SimpleKotlinClass.json @@ -39,7 +39,7 @@ }, "supertypes": [ { - "internalName": "kotlin/Any" + "internalName": "java/lang/Object" } ] } \ No newline at end of file