mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-268316 Java: provide API in JvmBytecodeAnalysis to collect implicit superclass references
Before, JvmBytecodeAnalysis processed only references explicitly mentioned in *.class files. However, javac requires having all superclasses of the classes being compiled and the classes methods from which are called in the classpath. In such cases, 'Extract Module' action and a quick-fix for extracting module in DevKit (IJPL-227635) didn't add required dependencies to the extracted module, so code fails to compile. Here a new API JvmBytecodeAnalysis.createReferenceAnalyzerWithImplicitSuperclassReferences is introduced which can be used to take into account such implicit references as well. GitOrigin-RevId: e4dec4e63d3a5ed70012747d386a48628275b6cb
This commit is contained in:
committed by
intellij-monorepo-bot
parent
c5f9fa5d6a
commit
f2425bec74
@@ -24,6 +24,14 @@ public interface JvmBytecodeAnalysis {
|
||||
@Contract(pure = true)
|
||||
public fun createReferenceAnalyzer(processor: JvmBytecodeReferenceProcessor): ClassFileAnalyzer
|
||||
|
||||
/**
|
||||
* Creates a new instance of [ClassFileAnalyzer] which will process references in *.class files.
|
||||
* It also processes references to superclasses and superinterfaces which aren't mentioned in *.class files directly, but will be accessed
|
||||
* by the compiler and therefore need to be available in the compilation classpath.
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
public fun createReferenceAnalyzerWithImplicitSuperclassReferences(processor: JvmBytecodeReferenceProcessor, classpath: List<Path>): ClassFileAnalyzer
|
||||
|
||||
/**
|
||||
* Creates a new instance of [ClassFileAnalyzer] which will process declarations in *.class files.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
package com.intellij.java.analysis.bytecode
|
||||
|
||||
/**
|
||||
* Pass an instance of this interface to [JvmBytecodeAnalysis.createReferenceAnalyzer] or
|
||||
* Pass an instance of this interface to [JvmBytecodeAnalysis.createReferenceAnalyzerWithImplicitSuperclassReferences] or
|
||||
* [JvmBytecodeAnalysis.createDeclarationAndReferencesAnalyzer] to process references in *.class files.
|
||||
*/
|
||||
public interface JvmBytecodeReferenceProcessor {
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.java.analysis.impl.bytecode
|
||||
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.diagnostic.trace
|
||||
import org.jetbrains.org.objectweb.asm.ClassReader
|
||||
import org.jetbrains.org.objectweb.asm.ClassVisitor
|
||||
import org.jetbrains.org.objectweb.asm.Opcodes
|
||||
import org.jetbrains.org.objectweb.asm.signature.SignatureReader
|
||||
import org.jetbrains.org.objectweb.asm.signature.SignatureVisitor
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readBytes
|
||||
|
||||
/**
|
||||
* Provides a way to recursively find all superclasses and classes mentioned in their generic signatures for a given class
|
||||
*/
|
||||
internal class ClassAncestorResolver(private val classpath: List<Path>) {
|
||||
private val packageToClasspathRoots by lazy {
|
||||
collectPackagesInClasspath(classpath)
|
||||
}
|
||||
private val ancestorsCache = HashMap<String, List<String>>()
|
||||
|
||||
fun getAllAncestors(binaryClassName: String): List<String> {
|
||||
val cached = ancestorsCache[binaryClassName]
|
||||
if (cached != null) return cached
|
||||
ancestorsCache[binaryClassName] = emptyList() //this is needed to avoid StackOverflowError if the class refers to itself from generic parameters
|
||||
val ancestors = collectAllAncestors(binaryClassName)
|
||||
ancestorsCache[binaryClassName] = ancestors
|
||||
return ancestors
|
||||
}
|
||||
|
||||
private fun collectAllAncestors(binaryClassName: String): List<String> {
|
||||
LOG.trace { "Collecting ancestors for $binaryClassName" }
|
||||
val superclasses = collectSuperclasses(binaryClassName)
|
||||
val result = HashSet<String>()
|
||||
for (superclass in superclasses) {
|
||||
result.add(superclass)
|
||||
result.addAll(getAllAncestors(superclass))
|
||||
}
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
private fun collectPackagesInClasspath(classpath: List<Path>): Map<String, List<Path>> {
|
||||
return classpath
|
||||
.asSequence()
|
||||
.flatMap { root ->
|
||||
withClassRootEntries(root) {
|
||||
it.map { entry -> root to entry.entryName.substringBeforeLast(delimiter = '/', missingDelimiterValue = "") }.toSet()
|
||||
}
|
||||
}
|
||||
.groupBy({ it.second }, { it.first })
|
||||
}
|
||||
|
||||
private fun collectSuperclasses(binaryClassName: String): List<String> {
|
||||
val packageName = binaryClassName.substringBeforeLast(delimiter = '/', missingDelimiterValue = "")
|
||||
val relativePath = "$binaryClassName.class"
|
||||
val paths = packageToClasspathRoots[packageName] ?: return emptyList()
|
||||
return paths
|
||||
.firstNotNullOfOrNull { root ->
|
||||
withClassRoot(root) { classRoot ->
|
||||
val classFilePath = classRoot.resolve(relativePath)
|
||||
if (classFilePath.exists()) loadSuperclasses(classFilePath)
|
||||
else null
|
||||
}
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
private fun loadSuperclasses(path: Path): List<String> {
|
||||
val result = ArrayList<String>()
|
||||
val visitor = object : ClassVisitor(Opcodes.API_VERSION, ) {
|
||||
override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String?>?) {
|
||||
if (superName != null) {
|
||||
result.add(superName)
|
||||
}
|
||||
if (interfaces != null) {
|
||||
Collections.addAll(result, *interfaces)
|
||||
}
|
||||
if (signature != null) {
|
||||
SignatureReader(signature).accept(object : SignatureVisitor(Opcodes.API_VERSION) {
|
||||
override fun visitClassType(name: String) {
|
||||
result.add(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
ClassReader(path.readBytes()).accept(visitor, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private val LOG = logger<ClassAncestorResolver>()
|
||||
+14
-1
@@ -21,14 +21,17 @@ final class ClassFileAnalysisVisitor extends ClassVisitor {
|
||||
private final DependencyFieldVisitor myFieldVisitor = new DependencyFieldVisitor();
|
||||
private final @Nullable JvmBytecodeDeclarationProcessor myDeclarationProcessor;
|
||||
private final @Nullable JvmBytecodeReferenceProcessor myReferenceProcessor;
|
||||
private final @Nullable ClassAncestorResolver myImplicitAncestorReferencesResolver;
|
||||
|
||||
private JvmClassBytecodeDeclaration myCurrentClass;
|
||||
|
||||
ClassFileAnalysisVisitor(@Nullable JvmBytecodeDeclarationProcessor declarationProcessor,
|
||||
@Nullable JvmBytecodeReferenceProcessor referenceProcessor) {
|
||||
@Nullable JvmBytecodeReferenceProcessor referenceProcessor,
|
||||
@Nullable ClassAncestorResolver implicitAncestorReferencesResolver) {
|
||||
super(Opcodes.API_VERSION);
|
||||
myDeclarationProcessor = declarationProcessor;
|
||||
myReferenceProcessor = referenceProcessor;
|
||||
myImplicitAncestorReferencesResolver = implicitAncestorReferencesResolver;
|
||||
}
|
||||
|
||||
void processFileContent(byte @NotNull [] classFileContent) {
|
||||
@@ -62,6 +65,15 @@ final class ClassFileAnalysisVisitor extends ClassVisitor {
|
||||
else {
|
||||
addSignature(signature);
|
||||
}
|
||||
processImplicitSuperclassReferences(myCurrentClass);
|
||||
}
|
||||
|
||||
private void processImplicitSuperclassReferences(JvmClassBytecodeDeclaration referencedClass) {
|
||||
if (myReferenceProcessor != null && myImplicitAncestorReferencesResolver != null) {
|
||||
for (String superclass : myImplicitAncestorReferencesResolver.getAllAncestors(referencedClass.getBinaryClassName())) {
|
||||
myReferenceProcessor.processClassReference(getOrCreateClassDeclaration(superclass), myCurrentClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<String, JvmClassBytecodeDeclaration> myClassDeclarations = new HashMap<>();
|
||||
@@ -186,6 +198,7 @@ final class ClassFileAnalysisVisitor extends ClassVisitor {
|
||||
JvmClassBytecodeDeclaration targetClass = addName(owner);
|
||||
addMethodDesc(desc);
|
||||
processMethodReference(targetClass, name, desc);
|
||||
processImplicitSuperclassReferences(targetClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+6
-5
@@ -12,9 +12,10 @@ import kotlin.io.path.*
|
||||
|
||||
internal class ClassFileAnalyzerImpl(
|
||||
declarationProcessor: JvmBytecodeDeclarationProcessor?,
|
||||
referenceProcessor: JvmBytecodeReferenceProcessor?
|
||||
referenceProcessor: JvmBytecodeReferenceProcessor?,
|
||||
implicitAncestorReferencesResolver: ClassAncestorResolver? = null,
|
||||
) : ClassFileAnalyzer {
|
||||
private val visitor = ClassFileAnalysisVisitor(declarationProcessor, referenceProcessor)
|
||||
private val visitor = ClassFileAnalysisVisitor(declarationProcessor, referenceProcessor, implicitAncestorReferencesResolver)
|
||||
|
||||
override fun processFile(path: Path) {
|
||||
// ASM ClassReader in any case reads the whole file into memory
|
||||
@@ -43,10 +44,10 @@ internal class ClassFileAnalyzerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private data class ClassFileEntry(val entryName: String, val path: Path)
|
||||
internal data class ClassFileEntry(val entryName: String, val path: Path)
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
private fun <R> withClassRootEntries(classRoot: Path, block: (entries: Sequence<ClassFileEntry>) -> R): R {
|
||||
internal fun <R> withClassRootEntries(classRoot: Path, block: (entries: Sequence<ClassFileEntry>) -> R): R {
|
||||
return withClassRoot(classRoot) { nioRoot ->
|
||||
val sequence = nioRoot
|
||||
.walk()
|
||||
@@ -58,7 +59,7 @@ private fun <R> withClassRootEntries(classRoot: Path, block: (entries: Sequence<
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R> withClassRoot(classRoot: Path, block: (root: Path) -> R): R {
|
||||
internal fun <R> withClassRoot(classRoot: Path, block: (root: Path) -> R): R {
|
||||
return when {
|
||||
classRoot.isDirectory() -> block(classRoot)
|
||||
classRoot.isRegularFile() && classRoot.extension == "jar" -> {
|
||||
|
||||
+14
-2
@@ -5,14 +5,26 @@ import com.intellij.java.analysis.bytecode.ClassFileAnalyzer
|
||||
import com.intellij.java.analysis.bytecode.JvmBytecodeAnalysis
|
||||
import com.intellij.java.analysis.bytecode.JvmBytecodeDeclarationProcessor
|
||||
import com.intellij.java.analysis.bytecode.JvmBytecodeReferenceProcessor
|
||||
import java.nio.file.Path
|
||||
|
||||
public class JvmBytecodeAnalysisImpl : JvmBytecodeAnalysis {
|
||||
override fun createReferenceAnalyzer(processor: JvmBytecodeReferenceProcessor): ClassFileAnalyzer {
|
||||
return ClassFileAnalyzerImpl(null, processor)
|
||||
return ClassFileAnalyzerImpl(declarationProcessor = null, referenceProcessor = processor)
|
||||
}
|
||||
|
||||
override fun createReferenceAnalyzerWithImplicitSuperclassReferences(
|
||||
processor: JvmBytecodeReferenceProcessor,
|
||||
classpath: List<Path>,
|
||||
): ClassFileAnalyzer {
|
||||
return ClassFileAnalyzerImpl(
|
||||
declarationProcessor = null,
|
||||
referenceProcessor = processor,
|
||||
implicitAncestorReferencesResolver = ClassAncestorResolver(classpath)
|
||||
)
|
||||
}
|
||||
|
||||
override fun createDeclarationAnalyzer(processor: JvmBytecodeDeclarationProcessor): ClassFileAnalyzer {
|
||||
return ClassFileAnalyzerImpl(processor, null)
|
||||
return ClassFileAnalyzerImpl(declarationProcessor = processor, referenceProcessor = null)
|
||||
}
|
||||
|
||||
override fun createDeclarationAndReferencesAnalyzer(
|
||||
|
||||
+125
-5
@@ -3,12 +3,21 @@ package com.intellij.java.analysis.bytecode
|
||||
|
||||
import com.intellij.compiler.JavaInMemoryCompiler
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import com.intellij.testFramework.rules.TempDirectoryExtension
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createParentDirectories
|
||||
import kotlin.io.path.writeBytes
|
||||
|
||||
@TestApplication
|
||||
internal class JvmBytecodeReferenceAnalysisTest {
|
||||
@JvmField
|
||||
@RegisterExtension
|
||||
val tempDirectory = TempDirectoryExtension()
|
||||
|
||||
@Test
|
||||
fun `test field reference`() {
|
||||
assertReference("public class Main { String field; }", "java/lang/String")
|
||||
@@ -52,6 +61,7 @@ internal class JvmBytecodeReferenceAnalysisTest {
|
||||
"A" to "public @interface A { Class<?> value(); }",
|
||||
"Main" to "@A(String.class) public class Main { }"
|
||||
),
|
||||
emptyMap(),
|
||||
"A", "java/lang/String"
|
||||
)
|
||||
}
|
||||
@@ -126,13 +136,113 @@ internal class JvmBytecodeReferenceAnalysisTest {
|
||||
"A")
|
||||
}
|
||||
|
||||
private fun assertReference(@Language("JAVA") source: String, vararg expectedTargets: String) {
|
||||
assertReference(mapOf("Main" to source), *expectedTargets)
|
||||
@Test
|
||||
fun `test implicit references to superclasses from declaration`() {
|
||||
assertReference(
|
||||
sources = mapOf(
|
||||
"Main" to "public class Main extends p.A {}"
|
||||
),
|
||||
sourcesToSearchImplicitReferences = mapOf(
|
||||
"p.A" to "package p; public class A extends Exception {}",
|
||||
),
|
||||
"p/A", "java/lang/Exception",
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertReference(sources: Map<String, String>, vararg expectedTargets: String) {
|
||||
val compiler = JavaInMemoryCompiler()
|
||||
@Test
|
||||
fun `test implicit references to superclasses from method call`() {
|
||||
assertReference(
|
||||
sources = mapOf(
|
||||
"p1.A" to "package p1; public class A extends p2.B { public void foo() {} }",
|
||||
"Main" to "public class Main { Main() { new p1.A().foo(); } }"
|
||||
),
|
||||
sourcesToSearchImplicitReferences = mapOf(
|
||||
"p2.B" to "package p2; public class B {}",
|
||||
),
|
||||
"p1/A", "p2/B",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test no implicit references to superclasses if instance is just passed around`() {
|
||||
assertReference(
|
||||
sources = mapOf(
|
||||
"p1.A" to "package p1; public class A extends p2.B { public void foo() {} }",
|
||||
"p1.AManager" to """
|
||||
|package p1;
|
||||
|public class AManager {
|
||||
| public A createInstance() { return new A(); }
|
||||
| public void useInstance(A a) { a.foo(); }
|
||||
|}
|
||||
""".trimMargin(),
|
||||
"Main" to """
|
||||
|import p1.*;
|
||||
|public class Main {
|
||||
| Main() {
|
||||
| AManager manager = new AManager();
|
||||
| A a = manager.createInstance();
|
||||
| manager.useInstance(a);
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
),
|
||||
sourcesToSearchImplicitReferences = mapOf(
|
||||
"p2.B" to "package p2; public class B {}",
|
||||
),
|
||||
"p1/A", "p1/AManager",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test implicit references to superclasses in generic parameters from declaration`() {
|
||||
assertReference(
|
||||
sources = mapOf(
|
||||
"Main" to "public abstract class Main extends p.A {}"
|
||||
),
|
||||
sourcesToSearchImplicitReferences = mapOf(
|
||||
"p.A" to "package p; public abstract class A extends B<String> {}",
|
||||
"p.B" to "package p; import java.util.*; public abstract class B<T> implements Map<T, List<T>> {}"
|
||||
),
|
||||
"p/A", "p/B", "java/util/Map", "java/util/List", "java/lang/String",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test avoid StackOverflowError when processing implicit references to superclasses`() {
|
||||
assertReference(
|
||||
sources = mapOf(
|
||||
"Main" to "public abstract class Main extends p.A {}"
|
||||
),
|
||||
sourcesToSearchImplicitReferences = mapOf(
|
||||
"p.A" to "package p; public class A extends B<A> {}",
|
||||
"p.B" to "package p; public class B<T> {}"
|
||||
),
|
||||
"p/A", "p/B",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun assertReference(@Language("JAVA") source: String, vararg expectedTargets: String) {
|
||||
assertReference(
|
||||
sources = mapOf("Main" to source),
|
||||
sourcesToSearchImplicitReferences = emptyMap(),
|
||||
expectedTargets = expectedTargets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertReference(sources: Map<String, String>,
|
||||
sourcesToSearchImplicitReferences: Map<String, String> = emptyMap(),
|
||||
vararg expectedTargets: String) {
|
||||
val classpathForImplicitReferences = ArrayList<Path>()
|
||||
if (sourcesToSearchImplicitReferences.isNotEmpty()) {
|
||||
saveClassFiles(JavaInMemoryCompiler().compile(sourcesToSearchImplicitReferences))
|
||||
classpathForImplicitReferences.add(tempDirectory.rootPath)
|
||||
}
|
||||
val compiler = JavaInMemoryCompiler(*classpathForImplicitReferences.map { it.toFile() }.toTypedArray())
|
||||
val compiledClasses = compiler.compile(sources)
|
||||
if (sourcesToSearchImplicitReferences.isNotEmpty()) {
|
||||
saveClassFiles(compiledClasses)
|
||||
}
|
||||
val mainClassBytes = compiledClasses["Main"] ?: throw IllegalStateException("Main class not found")
|
||||
|
||||
val analysis = JvmBytecodeAnalysis.getInstance()
|
||||
@@ -143,9 +253,19 @@ internal class JvmBytecodeReferenceAnalysisTest {
|
||||
}
|
||||
}
|
||||
|
||||
val analyzer = analysis.createReferenceAnalyzer(referenceProcessor)
|
||||
val analyzer =
|
||||
if (classpathForImplicitReferences.isEmpty()) analysis.createReferenceAnalyzer(referenceProcessor)
|
||||
else analysis.createReferenceAnalyzerWithImplicitSuperclassReferences(referenceProcessor, classpathForImplicitReferences)
|
||||
analyzer.processFileContent(mainClassBytes)
|
||||
references.remove("java/lang/Object") //always referenced as the superclass
|
||||
assertThat(references).containsExactlyInAnyOrder(*expectedTargets)
|
||||
}
|
||||
|
||||
private fun saveClassFiles(classFiles: Map<String, ByteArray>) {
|
||||
for ((className, classFileContent) in classFiles) {
|
||||
val classFilePath = tempDirectory.rootPath.resolve("${className.replace('.', '/')}.class")
|
||||
classFilePath.createParentDirectories()
|
||||
classFilePath.writeBytes(classFileContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user