You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/10/04 07:05:05 UTC
(maven-compiler-plugin) branch feature/abi-incremental updated: fix: commit missing refactoring — remove AbiExtractor/AsmClassAnalyzer/CompilationAnalyzer/DependencyScanner, consolidate into BytecodeAnalyzer/ClassfileClassAnalyzer
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch feature/abi-incremental
in repository https://gitbox.apache.org/repos/asf/maven-compiler-plugin.git
The following commit(s) were added to refs/heads/feature/abi-incremental by this push:
new b0d10f03 fix: commit missing refactoring — remove AbiExtractor/AsmClassAnalyzer/CompilationAnalyzer/DependencyScanner, consolidate into BytecodeAnalyzer/ClassfileClassAnalyzer
b0d10f03 is described below
commit b0d10f039dc1da75eb17f5013cbe57d22f3b726b
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Sun Oct 4 07:04:59 2026 +0000
fix: commit missing refactoring — remove AbiExtractor/AsmClassAnalyzer/CompilationAnalyzer/DependencyScanner, consolidate into BytecodeAnalyzer/ClassfileClassAnalyzer
---
.../apache/maven/plugin/compiler/ToolExecutor.java | 62 ++--
.../plugin/compiler/incremental/AbiExtractor.java | 299 -------------------
.../compiler/incremental/AsmClassAnalyzer.java | 204 -------------
.../compiler/incremental/BytecodeAnalyzer.java | 74 +++--
.../plugin/compiler/incremental/ClassAnalyzer.java | 11 +-
.../compiler/incremental/CompilationAnalyzer.java | 226 --------------
.../compiler/incremental/DependencyScanner.java | 209 -------------
.../maven/plugin/compiler/incremental/Sha256.java | 6 +-
.../incremental/ClassfileClassAnalyzer.java | 326 +++++++++++++++++----
.../compiler/incremental/AbiExtractorTest.java | 150 ----------
.../compiler/incremental/BytecodeAnalyzerTest.java | 44 ++-
.../compiler/incremental/CompilerTestHelper.java | 38 ++-
.../incremental/DependencyScannerTest.java | 142 ---------
13 files changed, 393 insertions(+), 1398 deletions(-)
diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
index 043f5c2b..cc2854c3 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
@@ -50,7 +50,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
-import com.sun.source.util.JavacTask;
import org.apache.maven.api.JavaPathType;
import org.apache.maven.api.PathType;
import org.apache.maven.api.plugin.Log;
@@ -58,6 +57,7 @@ import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.services.DependencyResolverResult;
import org.apache.maven.api.services.MavenException;
import org.apache.maven.plugin.compiler.incremental.AbiIncrementalBuild;
+import org.apache.maven.plugin.compiler.incremental.Sha256;
/**
* A task which configures and executes a Java tool such as the Java compiler.
@@ -932,23 +932,6 @@ public class ToolExecutor {
*/
void compileWithAbiIncremental(JavaCompiler compiler, final Options configuration, final AbstractCompilerMojo mojo)
throws IOException {
- if (compiler instanceof ForkedTool) {
- logger.warn("ABI incremental strategy is not supported with forked compilation."
- + " Falling back to full compilation.");
- // ABI analysis requires in-process javac (JavacTask). Fall back to the
- // standard incremental strategy instead of attempting ABI analysis.
- var compilerOutput = new StringWriter();
- applyIncrementalBuild(mojo, configuration);
- boolean success = compile(compiler, configuration, compilerOutput);
- String output = compilerOutput.toString();
- if (!output.isBlank()) {
- logger.warn(output);
- }
- if (!success) {
- throw new CompilationFailureException("Compilation failed (forked, non-ABI fallback).");
- }
- return;
- }
var abiBuild = new AbiIncrementalBuild(outputDirectory);
// Collect classpath entries for cross-module tracking
@@ -1021,9 +1004,18 @@ public class ToolExecutor {
var originalSourceFiles = new ArrayList<>(sourceFiles);
boolean success = true;
+ // Safety bound: the compile set is monotonically growing (bounded by total source count).
+ // If a bug causes processCompiledClasses to return files already compiled, this prevents
+ // an infinite loop. In practice this limit should never be reached.
+ int maxRounds = originalSourceFiles.size() + 1;
+ int rounds = 0;
try {
while (!toCompile.isEmpty()) {
+ if (++rounds > maxRounds) {
+ throw new IllegalStateException("ABI cascade loop did not converge after " + maxRounds
+ + " rounds — " + "possible dependency cycle or bug in processCompiledClasses()");
+ }
Set<Path> compileSet = toCompile;
sourceFiles = originalSourceFiles.stream()
.filter(sf -> compileSet.contains(sf.file))
@@ -1043,7 +1035,7 @@ public class ToolExecutor {
break;
}
- toCompile = abiBuild.processRound();
+ toCompile = abiBuild.processCompiledClasses(compileSet);
if (!toCompile.isEmpty()) {
logger.info("ABI cascade: recompiling " + toCompile.size() + " additional file(s).");
if (mojo.showCompilationChanges) {
@@ -1080,14 +1072,11 @@ public class ToolExecutor {
if (Files.isRegularFile(patchFile)) {
try {
byte[] content = Files.readAllBytes(patchFile);
- var md = java.security.MessageDigest.getInstance("SHA-256");
- byte[] hash = md.digest(content);
- var hex = new StringBuilder();
- for (byte b : hash) {
- hex.append(String.format("%02x", b));
- }
- digest.append(patchFile).append(':').append(hex, 0, 16).append(';');
- } catch (IOException | java.security.NoSuchAlgorithmException e) {
+ digest.append(patchFile)
+ .append(':')
+ .append(Sha256.hash(content))
+ .append(';');
+ } catch (IOException e) {
digest.append(patchFile).append(":unreadable;");
}
}
@@ -1095,17 +1084,7 @@ public class ToolExecutor {
if (digest.isEmpty()) {
return "";
}
- try {
- var md = java.security.MessageDigest.getInstance("SHA-256");
- byte[] hash = md.digest(digest.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
- var hex = new StringBuilder();
- for (byte b : hash) {
- hex.append(String.format("%02x", b));
- }
- return hex.substring(0, 16);
- } catch (java.security.NoSuchAlgorithmException e) {
- return digest.toString();
- }
+ return Sha256.hash(digest.toString());
}
/**
@@ -1148,7 +1127,6 @@ public class ToolExecutor {
case PACKAGE_WITH_MODULE, MODULE_SOURCE -> new ModulePathManager(fileManager);
};
- boolean isBaseRelease = true;
for (final SourcesForRelease unit : units) {
configuration.setRelease(unit.getReleaseString());
pathManager.configureSourcePaths(unit.roots);
@@ -1160,17 +1138,11 @@ public class ToolExecutor {
Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjectsFromPaths(unit.files);
JavaCompiler.CompilationTask task;
task = compiler.getTask(otherOutput, fileManager, listener, configuration.options, null, sources);
- // Only attach ABI analyzer for the base release — versioned
- // releases (multi-release JARs) are always compiled fully
- if (isBaseRelease && task instanceof JavacTask javacTask) {
- abiBuild.attachTo(javacTask);
- }
success = task.call();
if (!success) {
break;
}
}
- isBaseRelease = false;
pathManager.markVersioned();
}
} catch (UncheckedIOException e) {
diff --git a/src/main/java/org/apache/maven/plugin/compiler/incremental/AbiExtractor.java b/src/main/java/org/apache/maven/plugin/compiler/incremental/AbiExtractor.java
deleted file mode 100644
index ce93ae70..00000000
--- a/src/main/java/org/apache/maven/plugin/compiler/incremental/AbiExtractor.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.plugin.compiler.incremental;
-
-import javax.lang.model.element.Element;
-import javax.lang.model.element.ElementKind;
-import javax.lang.model.element.ExecutableElement;
-import javax.lang.model.element.Modifier;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.element.VariableElement;
-import javax.lang.model.type.TypeKind;
-import javax.lang.model.type.TypeMirror;
-
-import java.util.Comparator;
-import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
-
-import com.sun.source.tree.DirectiveTree;
-import com.sun.source.tree.ExportsTree;
-import com.sun.source.tree.ModuleTree;
-import com.sun.source.tree.OpensTree;
-import com.sun.source.tree.ProvidesTree;
-import com.sun.source.tree.RequiresTree;
-import com.sun.source.tree.UsesTree;
-
-/**
- * Computes ABI (Application Binary Interface) fingerprints for Java types
- * using the {@link javax.lang.model} element API.
- *
- * <p>The canonical form includes the type's modifiers, name, type parameters,
- * superclass, interfaces, and all non-private members (fields, constructors,
- * methods, nested types). For {@code static final} fields, the compile-time
- * constant value is included so that changes to inlined constants are detected.
- *
- * <p>Also computes fingerprints for module descriptors ({@code module-info.java})
- * based on sorted {@code requires}, {@code exports}, {@code opens}, {@code uses},
- * and {@code provides} directives.
- *
- * <p>The fingerprint is a truncated SHA-256 hash of this canonical form.
- * Two types have the same fingerprint if and only if their public API surfaces
- * are identical — method body changes do not affect it.
- *
- * @see CompilationAnalyzer
- */
-public class AbiExtractor {
-
- public static String computeFingerprint(TypeElement type) {
- return Sha256.hash(canonicalForm(type));
- }
-
- public static String canonicalForm(TypeElement type) {
- var sb = new StringBuilder();
- appendType(sb, type, 0);
- return sb.toString();
- }
-
- public static String computeModuleFingerprint(ModuleTree moduleTree) {
- return Sha256.hash(moduleCanonicalForm(moduleTree));
- }
-
- public static String moduleCanonicalForm(ModuleTree moduleTree) {
- var sb = new StringBuilder();
- if (moduleTree.getModuleType() == ModuleTree.ModuleKind.OPEN) {
- sb.append("open ");
- }
- sb.append("module ").append(moduleTree.getName()).append('\n');
-
- var requires = new TreeSet<String>();
- var exports = new TreeSet<String>();
- var opens = new TreeSet<String>();
- var uses = new TreeSet<String>();
- var provides = new TreeSet<String>();
-
- for (DirectiveTree directive : moduleTree.getDirectives()) {
- if (directive instanceof RequiresTree r) {
- var entry = new StringBuilder("requires ");
- if (r.isTransitive()) {
- entry.append("transitive ");
- }
- if (r.isStatic()) {
- entry.append("static ");
- }
- entry.append(r.getModuleName());
- requires.add(entry.toString());
- } else if (directive instanceof ExportsTree e) {
- var entry = new StringBuilder("exports ").append(e.getPackageName());
- var moduleNames = e.getModuleNames();
- if (moduleNames != null && !moduleNames.isEmpty()) {
- entry.append(" to ");
- entry.append(
- moduleNames.stream().map(Object::toString).sorted().collect(Collectors.joining(", ")));
- }
- exports.add(entry.toString());
- } else if (directive instanceof OpensTree o) {
- var entry = new StringBuilder("opens ").append(o.getPackageName());
- var moduleNames = o.getModuleNames();
- if (moduleNames != null && !moduleNames.isEmpty()) {
- entry.append(" to ");
- entry.append(
- moduleNames.stream().map(Object::toString).sorted().collect(Collectors.joining(", ")));
- }
- opens.add(entry.toString());
- } else if (directive instanceof UsesTree u) {
- uses.add("uses " + u.getServiceName());
- } else if (directive instanceof ProvidesTree p) {
- var entry = new StringBuilder("provides ").append(p.getServiceName());
- var implNames = p.getImplementationNames();
- if (implNames != null && !implNames.isEmpty()) {
- entry.append(" with ");
- entry.append(
- implNames.stream().map(Object::toString).sorted().collect(Collectors.joining(", ")));
- }
- provides.add(entry.toString());
- }
- }
-
- for (String r : requires) {
- sb.append(" ").append(r).append('\n');
- }
- for (String e : exports) {
- sb.append(" ").append(e).append('\n');
- }
- for (String o : opens) {
- sb.append(" ").append(o).append('\n');
- }
- for (String u : uses) {
- sb.append(" ").append(u).append('\n');
- }
- for (String p : provides) {
- sb.append(" ").append(p).append('\n');
- }
-
- return sb.toString();
- }
-
- private static void appendType(StringBuilder sb, TypeElement type, int indent) {
- String prefix = " ".repeat(indent);
-
- sb.append(prefix);
- appendModifiers(sb, type.getModifiers());
- sb.append(type.getKind().toString().toLowerCase(java.util.Locale.ROOT)).append(' ');
- sb.append(type.getQualifiedName());
-
- var typeParams = type.getTypeParameters();
- if (!typeParams.isEmpty()) {
- sb.append('<');
- sb.append(typeParams.stream()
- .map(tp -> {
- var tpSb = new StringBuilder(tp.getSimpleName());
- var bounds = tp.getBounds().stream()
- .filter(b -> !"java.lang.Object".equals(b.toString()))
- .map(TypeMirror::toString)
- .sorted()
- .toList();
- if (!bounds.isEmpty()) {
- tpSb.append(" extends ");
- tpSb.append(String.join(" & ", bounds));
- }
- return tpSb.toString();
- })
- .collect(Collectors.joining(", ")));
- sb.append('>');
- }
- sb.append('\n');
-
- TypeMirror superclass = type.getSuperclass();
- if (superclass.getKind() != TypeKind.NONE && !"java.lang.Object".equals(superclass.toString())) {
- sb.append(prefix).append(" extends ").append(superclass).append('\n');
- }
-
- for (TypeMirror iface : type.getInterfaces()) {
- sb.append(prefix).append(" implements ").append(iface).append('\n');
- }
-
- List<? extends TypeMirror> permitted = type.getPermittedSubclasses();
- if (permitted != null && !permitted.isEmpty()) {
- var sorted = permitted.stream().map(TypeMirror::toString).sorted().toList();
- for (String p : sorted) {
- sb.append(prefix).append(" permits ").append(p).append('\n');
- }
- }
-
- // Enum constants are emitted first in declaration order (ordinal matters)
- for (Element el : type.getEnclosedElements()) {
- if (el.getKind() == ElementKind.ENUM_CONSTANT && !el.getModifiers().contains(Modifier.PRIVATE)) {
- appendField(sb, (VariableElement) el, indent + 1);
- }
- }
-
- var members = type.getEnclosedElements().stream()
- .filter(e -> !e.getModifiers().contains(Modifier.PRIVATE))
- .filter(e -> e.getKind() != ElementKind.ENUM_CONSTANT)
- .sorted(Comparator.<Element, Integer>comparing(e -> switch (e.getKind()) {
- case FIELD -> 0;
- case CONSTRUCTOR -> 1;
- case METHOD -> 2;
- default -> e.getKind().isClass() || e.getKind().isInterface() ? 3 : 4;
- })
- .thenComparing(e -> e.getSimpleName().toString())
- .thenComparing(e -> e instanceof ExecutableElement ee
- ? ee.getParameters().stream()
- .map(p -> p.asType().toString())
- .collect(Collectors.joining(","))
- : ""))
- .toList();
-
- for (Element member : members) {
- if (member instanceof VariableElement ve) {
- appendField(sb, ve, indent + 1);
- } else if (member instanceof ExecutableElement ee) {
- appendMethod(sb, ee, indent + 1);
- } else if (member instanceof TypeElement te) {
- appendType(sb, te, indent + 1);
- }
- }
- }
-
- private static void appendField(StringBuilder sb, VariableElement field, int indent) {
- sb.append(" ".repeat(indent));
- appendModifiers(sb, field.getModifiers());
- sb.append(field.asType()).append(' ');
- sb.append(field.getSimpleName());
- Object constValue = field.getConstantValue();
- if (constValue != null) {
- if (constValue instanceof String s) {
- sb.append(" = \"").append(s).append('"');
- } else {
- sb.append(" = ").append(constValue);
- }
- }
- sb.append('\n');
- }
-
- private static void appendMethod(StringBuilder sb, ExecutableElement method, int indent) {
- sb.append(" ".repeat(indent));
- appendModifiers(sb, method.getModifiers());
-
- var typeParams = method.getTypeParameters();
- if (!typeParams.isEmpty()) {
- sb.append('<');
- sb.append(
- typeParams.stream().map(tp -> tp.getSimpleName().toString()).collect(Collectors.joining(", ")));
- sb.append("> ");
- }
-
- if (method.getKind() != ElementKind.CONSTRUCTOR) {
- sb.append(method.getReturnType()).append(' ');
- }
-
- sb.append(method.getSimpleName()).append('(');
- sb.append(
- method.getParameters().stream().map(p -> p.asType().toString()).collect(Collectors.joining(", ")));
- sb.append(')');
-
- var thrown = method.getThrownTypes();
- if (!thrown.isEmpty()) {
- sb.append(" throws ");
- sb.append(thrown.stream().map(TypeMirror::toString).collect(Collectors.joining(", ")));
- }
- sb.append('\n');
- }
-
- private static void appendModifiers(StringBuilder sb, Set<Modifier> modifiers) {
- for (Modifier m : List.of(
- Modifier.PUBLIC,
- Modifier.PROTECTED,
- Modifier.ABSTRACT,
- Modifier.STATIC,
- Modifier.FINAL,
- Modifier.SYNCHRONIZED,
- Modifier.NATIVE,
- Modifier.STRICTFP,
- Modifier.DEFAULT,
- Modifier.SEALED,
- Modifier.NON_SEALED)) {
- if (modifiers.contains(m)) {
- sb.append(m).append(' ');
- }
- }
- }
-}
diff --git a/src/main/java/org/apache/maven/plugin/compiler/incremental/AsmClassAnalyzer.java b/src/main/java/org/apache/maven/plugin/compiler/incremental/AsmClassAnalyzer.java
deleted file mode 100644
index e1ffaf04..00000000
--- a/src/main/java/org/apache/maven/plugin/compiler/incremental/AsmClassAnalyzer.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.plugin.compiler.incremental;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
-
-import org.objectweb.asm.AnnotationVisitor;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.FieldVisitor;
-import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.Type;
-
-/**
- * {@link ClassAnalyzer} implementation using the bundled ASM library.
- *
- * <p>This implementation works on any JDK version supported by the plugin
- * (Java 17+) and is used as the default when the JVM version is below 24 or
- * when the {@code java.lang.classfile} API is not available.
- *
- * <p>Type references are collected from the class hierarchy, field/method
- * descriptors, exception types, annotations, and bytecode instructions
- * ({@code new}, {@code checkcast}, {@code instanceof}, field/method owners).
- *
- * <p>The ABI canonical form is derived from non-private, non-synthetic fields
- * and methods using erased types (no generics).
- *
- * @see BytecodeAnalyzer
- * @see ClassAnalyzer
- */
-class AsmClassAnalyzer extends ClassAnalyzer {
-
- @Override
- public BytecodeAnalyzer.ClassAnalysis analyze(byte[] classBytes) {
- var reader = new ClassReader(classBytes);
- var collector = new TypeCollector();
- reader.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
-
- String className = BytecodeAnalyzer.toJavaName(collector.className);
- collector.referencedTypes.remove(className);
-
- String abiCanonical = buildCanonical(reader, collector);
- String abiFingerprint = Sha256.hash(abiCanonical);
-
- return new BytecodeAnalyzer.ClassAnalysis(className, abiFingerprint, abiCanonical, collector.referencedTypes);
- }
-
- private static class TypeCollector extends ClassVisitor {
- String className;
- String superName;
- String classSignature;
- final Set<String> referencedTypes = new TreeSet<>();
- final List<String> interfaces = new ArrayList<>();
-
- TypeCollector() {
- super(Opcodes.ASM9);
- }
-
- @Override
- public void visit(
- int version, int access, String name, String signature, String superName, String[] interfaces) {
- this.className = name;
- this.superName = superName;
- this.classSignature = signature;
- if (superName != null) {
- addRef(superName);
- }
- if (interfaces != null) {
- for (String iface : interfaces) {
- this.interfaces.add(iface);
- addRef(iface);
- }
- }
- }
-
- @Override
- public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
- addDescriptorTypes(descriptor);
- return null;
- }
-
- @Override
- public MethodVisitor visitMethod(
- int access, String name, String descriptor, String signature, String[] exceptions) {
- addDescriptorTypes(descriptor);
- if (exceptions != null) {
- for (String ex : exceptions) {
- addRef(ex);
- }
- }
- return new MethodVisitor(Opcodes.ASM9) {
- @Override
- public void visitTypeInsn(int opcode, String type) {
- addRef(type);
- }
-
- @Override
- public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
- addRef(owner);
- }
-
- @Override
- public void visitMethodInsn(
- int opcode, String owner, String name, String descriptor, boolean isInterface) {
- addRef(owner);
- }
-
- @Override
- public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
- addDescriptorTypes(descriptor);
- }
- };
- }
-
- @Override
- public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
- addDescriptorTypes(descriptor);
- return null;
- }
-
- private void addRef(String internalName) {
- String resolved = resolveInternalName(internalName);
- if (resolved != null) {
- referencedTypes.add(resolved);
- }
- }
-
- private void addDescriptorTypes(String descriptor) {
- for (var type : Type.getArgumentTypes(descriptor.startsWith("(") ? descriptor : "()" + descriptor)) {
- addType(type);
- }
- if (descriptor.startsWith("(")) {
- addType(Type.getReturnType(descriptor));
- } else {
- addType(Type.getType(descriptor));
- }
- }
-
- private void addType(Type type) {
- if (type.getSort() == Type.ARRAY) {
- addType(type.getElementType());
- } else if (type.getSort() == Type.OBJECT) {
- referencedTypes.add(type.getClassName());
- }
- }
- }
-
- // --- ABI canonical form ---
-
- private static String buildCanonical(ClassReader reader, TypeCollector collector) {
- var fields = new ArrayList<FieldInfo>();
- var methods = new ArrayList<MethodInfo>();
-
- reader.accept(
- new ClassVisitor(Opcodes.ASM9) {
- @Override
- public FieldVisitor visitField(
- int access, String name, String descriptor, String signature, Object value) {
- if (!isPrivateOrSynthetic(access)) {
- fields.add(new FieldInfo(access, name, descriptor, value, signature));
- }
- return null;
- }
-
- @Override
- public MethodVisitor visitMethod(
- int access, String name, String descriptor, String signature, String[] exceptions) {
- if (!isPrivateOrSynthetic(access) && !"<clinit>".equals(name)) {
- methods.add(new MethodInfo(access, name, descriptor, signature));
- }
- return null;
- }
- },
- ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
-
- String className = BytecodeAnalyzer.toJavaName(collector.className);
- String superName = collector.superName != null ? BytecodeAnalyzer.toJavaName(collector.superName) : null;
- var ifaceNames =
- collector.interfaces.stream().map(BytecodeAnalyzer::toJavaName).toList();
-
- return buildCanonicalForm(
- reader.getAccess(), className, collector.classSignature, superName, ifaceNames, fields, methods);
- }
-}
diff --git a/src/main/java/org/apache/maven/plugin/compiler/incremental/BytecodeAnalyzer.java b/src/main/java/org/apache/maven/plugin/compiler/incremental/BytecodeAnalyzer.java
index 3100b3a4..cd8d5acb 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/incremental/BytecodeAnalyzer.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/incremental/BytecodeAnalyzer.java
@@ -22,38 +22,51 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Set;
-import java.util.logging.Logger;
/**
* Facade for analyzing compiled {@code .class} files to extract type references
* and compute bytecode-level ABI fingerprints.
*
- * <p>When the JVM is JDK 24 or later, the standard {@code java.lang.classfile}
- * API ({@code ClassfileClassAnalyzer}) is used automatically. On earlier JVMs
- * the bundled ASM library ({@link AsmClassAnalyzer}) is used as a fallback.
+ * <p>Requires JDK 24 or later — uses the standard {@code java.lang.classfile} API.
+ * Attempting to use this class on an older JDK throws {@link UnsupportedOperationException}
+ * at class-load time with a clear diagnostic message.
*
- * <p>The {@link ClassAnalysis} record carries the class name, ABI fingerprint
- * (a 16-character SHA-256 prefix), the human-readable canonical form that the
- * fingerprint is derived from, and the set of fully-qualified type names
- * referenced by the class.
+ * <p>The {@link ClassAnalysis} record carries the class name, ABI fingerprint,
+ * human-readable canonical form, and two classified sets of type references:
+ * {@link ClassAnalysis#signatureTypes()} (API surface) and
+ * {@link ClassAnalysis#implementationTypes()} (method body instructions only).
*
- * @see AsmClassAnalyzer
* @see ClassAnalyzer
*/
public final class BytecodeAnalyzer {
- private static final Logger LOGGER = Logger.getLogger(BytecodeAnalyzer.class.getName());
-
/**
* Analysis result for a single {@code .class} file.
*
- * @param className fully-qualified class name (dot-separated)
- * @param abiFingerprint 16-character hex SHA-256 prefix of the ABI canonical form
- * @param abiCanonical human-readable representation of the public API surface
- * @param referencedTypes fully-qualified names of all types referenced by this class
+ * @param className fully-qualified class name (dot-separated)
+ * @param abiFingerprint 16-character hex SHA-256 prefix of the ABI canonical form
+ * @param abiCanonical human-readable representation of the public API surface
+ * @param signatureTypes types appearing in public API surface (method/field descriptors,
+ * supertype, interfaces, exception types, annotation types)
+ * @param implementationTypes types appearing only in method body bytecode instructions
+ * (INVOKEVIRTUAL, NEW, CHECKCAST, field owners, etc.)
+ * @param annotationTypes fully-qualified names of annotation types present on the class
+ * or its members, used for annotation processor cascade decisions
+ * @param moduleName Java module name, or empty string if unnamed or non-modular
+ * @param isModuleInfo {@code true} if this represents {@code module-info.class}
+ * @param sourceFileName simple source file name from the {@code SourceFile} class file
+ * attribute (e.g. {@code "Foo.java"}); empty string if absent
*/
public record ClassAnalysis(
- String className, String abiFingerprint, String abiCanonical, Set<String> referencedTypes) {}
+ String className,
+ String abiFingerprint,
+ String abiCanonical,
+ Set<String> signatureTypes,
+ Set<String> implementationTypes,
+ Set<String> annotationTypes,
+ String moduleName,
+ boolean isModuleInfo,
+ String sourceFileName) {}
/** The {@link ClassAnalyzer} implementation chosen at class-load time. */
private static final ClassAnalyzer ANALYZER = selectAnalyzer();
@@ -136,19 +149,22 @@ public final class BytecodeAnalyzer {
// --- implementation selection ---
private static ClassAnalyzer selectAnalyzer() {
- if (Runtime.version().feature() >= 24) {
- try {
- Class<?> cls = Class.forName(
- "org.apache.maven.plugin.compiler.incremental.ClassfileClassAnalyzer",
- true,
- BytecodeAnalyzer.class.getClassLoader());
- var ctor = cls.getDeclaredConstructor();
- ctor.setAccessible(true);
- return (ClassAnalyzer) ctor.newInstance();
- } catch (Exception | LinkageError e) {
- LOGGER.fine(() -> "java.lang.classfile analyzer unavailable (" + e + "), falling back to ASM");
- }
+ if (Runtime.version().feature() < 24) {
+ throw new UnsupportedOperationException("The ABI incremental compilation strategy requires JDK 24 or later "
+ + "(running JDK " + Runtime.version().feature() + "). "
+ + "Use a different incrementalStrategy or upgrade your build JDK.");
+ }
+ try {
+ Class<?> cls = Class.forName(
+ "org.apache.maven.plugin.compiler.incremental.ClassfileClassAnalyzer",
+ true,
+ BytecodeAnalyzer.class.getClassLoader());
+ var ctor = cls.getDeclaredConstructor();
+ ctor.setAccessible(true);
+ return (ClassAnalyzer) ctor.newInstance();
+ } catch (Exception | LinkageError e) {
+ throw new UnsupportedOperationException(
+ "Failed to load the classfile analyzer despite JDK >= 24: " + e.getMessage(), e);
}
- return new AsmClassAnalyzer();
}
}
diff --git a/src/main/java/org/apache/maven/plugin/compiler/incremental/ClassAnalyzer.java b/src/main/java/org/apache/maven/plugin/compiler/incremental/ClassAnalyzer.java
index 3f44fa35..09318fe6 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/incremental/ClassAnalyzer.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/incremental/ClassAnalyzer.java
@@ -28,24 +28,17 @@ import java.util.Set;
/**
* Abstract base for analyzing compiled {@code .class} files.
*
- * <p>Implementations collect type references (API-specific) and extract member
- * metadata, then delegate canonical-form construction and fingerprinting to the
- * shared logic in this class.
- *
- * <p>Two built-in implementations are provided:
+ * <p>The single built-in implementation is:
* <ul>
- * <li>{@link AsmClassAnalyzer} — uses the bundled ASM library; works on any
- * supported JDK version (Java 17+).</li>
* <li>{@code ClassfileClassAnalyzer} — uses the standard
* {@code java.lang.classfile} API introduced in JDK 24; loaded
- * reflectively at runtime when the JVM is JDK 24 or later.</li>
+ * reflectively at runtime. Requires JDK 24 or later.</li>
* </ul>
*
* <p>Use {@link BytecodeAnalyzer#analyze(byte[])} which selects the best
* available implementation automatically.
*
* @see BytecodeAnalyzer
- * @see AsmClassAnalyzer
*/
public abstract class ClassAnalyzer {
diff --git a/src/main/java/org/apache/maven/plugin/compiler/incremental/CompilationAnalyzer.java b/src/main/java/org/apache/maven/plugin/compiler/incremental/CompilationAnalyzer.java
deleted file mode 100644
index d59f774e..00000000
--- a/src/main/java/org/apache/maven/plugin/compiler/incremental/CompilationAnalyzer.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.plugin.compiler.incremental;
-
-import javax.lang.model.element.AnnotationMirror;
-import javax.lang.model.element.ModuleElement;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.util.Elements;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-
-import com.sun.source.tree.ClassTree;
-import com.sun.source.tree.CompilationUnitTree;
-import com.sun.source.tree.DirectiveTree;
-import com.sun.source.tree.ModuleTree;
-import com.sun.source.tree.ProvidesTree;
-import com.sun.source.tree.UsesTree;
-import com.sun.source.util.JavacTask;
-import com.sun.source.util.TaskEvent;
-import com.sun.source.util.TaskListener;
-import com.sun.source.util.TreePath;
-import com.sun.source.util.Trees;
-
-/**
- * A {@link TaskListener} that intercepts javac's {@code ANALYZE} phase to extract
- * per-type dependency and ABI information from a compilation.
- *
- * <p>For each type element analyzed, it runs a {@link DependencyScanner} over the
- * compilation unit's AST to collect type references — classified as signature
- * dependencies (appear in the public API surface) or implementation dependencies
- * (body-only) — and uses {@link AbiExtractor} to compute an ABI fingerprint.
- * Results are collected into {@link SourceFileAnalysis} records accessible
- * via {@link #getResults()}.
- *
- * <p>Also handles {@code module-info.java} by extracting module directive
- * fingerprints via {@link AbiExtractor#computeModuleFingerprint}.
- *
- * <p>JDK-internal types ({@code java.*}, {@code javax.*}, {@code jdk.*},
- * {@code sun.*}) are filtered from the dependency sets since they never change
- * across incremental builds.
- */
-public class CompilationAnalyzer implements TaskListener {
-
- private final Trees trees;
- private final Elements elements;
- private final Map<String, SourceFileAnalysis> analyses = new LinkedHashMap<>();
-
- public CompilationAnalyzer(JavacTask task) {
- this.trees = Trees.instance(task);
- this.elements = task.getElements();
- }
-
- static final String MODULE_PREFIX = "module:";
-
- @Override
- public void finished(TaskEvent e) {
- if (e.getKind() != TaskEvent.Kind.ANALYZE) {
- return;
- }
-
- var cu = e.getCompilationUnit();
- if (cu == null) {
- return;
- }
-
- ModuleTree moduleTree = cu.getModule();
- if (moduleTree != null) {
- analyzeModule(moduleTree, cu);
- return;
- }
-
- TypeElement typeElement = e.getTypeElement();
- if (typeElement == null) {
- return;
- }
-
- String qualifiedName = typeElement.getQualifiedName().toString();
- String sourceFile = cu.getSourceFile().getName();
-
- // Scope the scan to only the ClassTree of the current type element.
- // Scanning the full CU would pollute each type's dependency set with
- // references from other top-level types in the same compilation unit
- // (over-cascading). Trees.getPath() gives us the TreePath rooted at
- // this type's ClassTree; DependencyScanner is a TreePathScanner so
- // scanning from that path restricts traversal to this type's subtree.
- var scanner = new DependencyScanner(trees);
- TreePath typePath = trees.getPath(typeElement);
- if (typePath != null && typePath.getLeaf() instanceof ClassTree) {
- scanner.scan(typePath, null);
- } else {
- // Fallback: scan the full CU (safe but may over-cascade)
- scanner.scan(cu, null);
- }
-
- String abiFingerprint = AbiExtractor.computeFingerprint(typeElement);
- String abiCanonical = AbiExtractor.canonicalForm(typeElement);
-
- var sigDeps = new TreeSet<>(scanner.getSignatureDeps());
- var implDeps = new TreeSet<>(scanner.getImplementationDeps());
-
- sigDeps.remove(qualifiedName);
- implDeps.remove(qualifiedName);
- implDeps.removeAll(sigDeps);
- sigDeps.removeIf(CompilationAnalyzer::isJdkType);
- implDeps.removeIf(CompilationAnalyzer::isJdkType);
-
- var annotationTypes = new TreeSet<String>();
- // Type-level annotations
- for (AnnotationMirror am : typeElement.getAnnotationMirrors()) {
- String annotName = am.getAnnotationType().toString();
- if (!isJdkType(annotName)) {
- annotationTypes.add(annotName);
- }
- }
- // Member-level annotations (methods, fields, constructors)
- for (javax.lang.model.element.Element member : typeElement.getEnclosedElements()) {
- for (AnnotationMirror am : member.getAnnotationMirrors()) {
- String annotName = am.getAnnotationType().toString();
- if (!isJdkType(annotName)) {
- annotationTypes.add(annotName);
- }
- }
- }
-
- String moduleName = resolveModuleName(typeElement);
-
- analyses.put(
- qualifiedName,
- new SourceFileAnalysis(
- qualifiedName,
- sourceFile,
- sigDeps,
- implDeps,
- abiFingerprint,
- abiCanonical,
- annotationTypes,
- moduleName));
- }
-
- private String resolveModuleName(TypeElement typeElement) {
- try {
- ModuleElement module = elements.getModuleOf(typeElement);
- if (module != null && !module.isUnnamed()) {
- return module.getQualifiedName().toString();
- }
- } catch (Exception e) {
- // Some javac versions may not support getModuleOf — fall back to empty
- }
- return "";
- }
-
- private void analyzeModule(ModuleTree moduleTree, CompilationUnitTree cu) {
- String moduleName = moduleTree.getName().toString();
- String qualifiedName = MODULE_PREFIX + moduleName;
- String sourceFile = cu.getSourceFile().getName();
-
- String abiFingerprint = AbiExtractor.computeModuleFingerprint(moduleTree);
- String abiCanonical = AbiExtractor.moduleCanonicalForm(moduleTree);
-
- var sigDeps = new TreeSet<String>();
- for (DirectiveTree directive : moduleTree.getDirectives()) {
- if (directive instanceof UsesTree u) {
- String typeName = u.getServiceName().toString();
- if (!isJdkType(typeName)) {
- sigDeps.add(typeName);
- }
- } else if (directive instanceof ProvidesTree p) {
- String typeName = p.getServiceName().toString();
- if (!isJdkType(typeName)) {
- sigDeps.add(typeName);
- }
- if (p.getImplementationNames() != null) {
- for (var impl : p.getImplementationNames()) {
- String implName = impl.toString();
- if (!isJdkType(implName)) {
- sigDeps.add(implName);
- }
- }
- }
- }
- }
-
- analyses.put(
- qualifiedName,
- new SourceFileAnalysis(
- qualifiedName,
- sourceFile,
- sigDeps,
- Set.of(),
- abiFingerprint,
- abiCanonical,
- Set.of(),
- moduleName));
- }
-
- public Map<String, SourceFileAnalysis> getResults() {
- return Collections.unmodifiableMap(analyses);
- }
-
- static boolean isJdkType(String name) {
- return name.startsWith("java.")
- || name.startsWith("javax.")
- || name.startsWith("jdk.")
- || name.startsWith("sun.");
- }
-}
diff --git a/src/main/java/org/apache/maven/plugin/compiler/incremental/DependencyScanner.java b/src/main/java/org/apache/maven/plugin/compiler/incremental/DependencyScanner.java
deleted file mode 100644
index b6bba866..00000000
--- a/src/main/java/org/apache/maven/plugin/compiler/incremental/DependencyScanner.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.plugin.compiler.incremental;
-
-import javax.lang.model.element.Element;
-import javax.lang.model.element.ExecutableElement;
-import javax.lang.model.element.Modifier;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.element.VariableElement;
-
-import java.util.Collections;
-import java.util.Set;
-import java.util.TreeSet;
-
-import com.sun.source.tree.AnnotationTree;
-import com.sun.source.tree.BlockTree;
-import com.sun.source.tree.ClassTree;
-import com.sun.source.tree.IdentifierTree;
-import com.sun.source.tree.ImportTree;
-import com.sun.source.tree.MemberSelectTree;
-import com.sun.source.tree.MethodTree;
-import com.sun.source.tree.VariableTree;
-import com.sun.source.util.TreePath;
-import com.sun.source.util.TreePathScanner;
-import com.sun.source.util.Trees;
-
-/**
- * Walks the AST after ANALYZE to extract type dependencies, classifying each
- * as signature-level (appears in public API) or implementation-level (body-only).
- */
-public class DependencyScanner extends TreePathScanner<Void, Void> {
-
- private final Trees trees;
- private final Set<String> signatureDeps = new TreeSet<>();
- private final Set<String> implementationDeps = new TreeSet<>();
- private int bodyDepth = 0;
- private boolean inPrivateMember = false;
-
- public DependencyScanner(Trees trees) {
- this.trees = trees;
- }
-
- public Set<String> getSignatureDeps() {
- return Collections.unmodifiableSet(signatureDeps);
- }
-
- public Set<String> getImplementationDeps() {
- return Collections.unmodifiableSet(implementationDeps);
- }
-
- @Override
- public Void visitImport(ImportTree node, Void p) {
- // Skip imports — they don't create dependencies, the actual usage sites do.
- // Without this, all imported types appear as signature deps regardless of
- // where they're actually used.
- return null;
- }
-
- @Override
- public Void visitMethod(MethodTree node, Void p) {
- Element el = resolveElement(getCurrentPath());
- boolean wasPrivate = inPrivateMember;
- if (el != null && el.getModifiers().contains(Modifier.PRIVATE)) {
- inPrivateMember = true;
- }
-
- // Signature parts: return type, parameters, throws, type params, annotations
- scan(node.getModifiers(), p);
- scan(node.getTypeParameters(), p);
- scan(node.getReturnType(), p);
- scan(node.getParameters(), p);
- scan(node.getThrows(), p);
- scan(node.getDefaultValue(), p);
-
- // Body: implementation context
- if (node.getBody() != null) {
- bodyDepth++;
- scan(node.getBody(), p);
- bodyDepth--;
- }
-
- inPrivateMember = wasPrivate;
- return null;
- }
-
- @Override
- public Void visitVariable(VariableTree node, Void p) {
- TreePath parentPath = getCurrentPath().getParentPath();
- if (parentPath != null && parentPath.getLeaf() instanceof ClassTree) {
- // Field: type is signature (if non-private), initializer is implementation
- Element el = resolveElement(getCurrentPath());
- boolean wasPrivate = inPrivateMember;
- if (el != null && el.getModifiers().contains(Modifier.PRIVATE)) {
- inPrivateMember = true;
- }
-
- scan(node.getModifiers(), p);
- scan(node.getType(), p);
-
- if (node.getInitializer() != null) {
- bodyDepth++;
- scan(node.getInitializer(), p);
- bodyDepth--;
- }
-
- inPrivateMember = wasPrivate;
- return null;
- }
- return super.visitVariable(node, p);
- }
-
- @Override
- public Void visitBlock(BlockTree node, Void p) {
- // Static/instance initializer blocks (direct children of ClassTree)
- TreePath parentPath = getCurrentPath().getParentPath();
- if (parentPath != null && parentPath.getLeaf() instanceof ClassTree) {
- bodyDepth++;
- var result = super.visitBlock(node, p);
- bodyDepth--;
- return result;
- }
- return super.visitBlock(node, p);
- }
-
- @Override
- public Void visitAnnotation(AnnotationTree node, Void p) {
- // Record the annotation type itself
- recordReference(getCurrentPath());
- // Visit annotation arguments — this catches types used in annotation values
- // such as @Foo(SomeEnum.VALUE) or @Foo(SomeType.class), where SomeEnum/SomeType
- // need to be tracked as dependencies even if not in any method signature.
- return super.visitAnnotation(node, p);
- }
-
- @Override
- public Void visitIdentifier(IdentifierTree node, Void p) {
- recordReference(getCurrentPath());
- return super.visitIdentifier(node, p);
- }
-
- @Override
- public Void visitMemberSelect(MemberSelectTree node, Void p) {
- recordReference(getCurrentPath());
- return super.visitMemberSelect(node, p);
- }
-
- private void recordReference(TreePath path) {
- Element element = resolveElement(path);
- if (element == null) {
- return;
- }
-
- TypeElement typeElement;
- if (element instanceof TypeElement te) {
- typeElement = te;
- } else if (element instanceof ExecutableElement ee) {
- typeElement = enclosingType(ee);
- } else if (element instanceof VariableElement ve) {
- typeElement = enclosingType(ve);
- } else {
- typeElement = null;
- }
-
- if (typeElement == null) {
- return;
- }
-
- String qname = typeElement.getQualifiedName().toString();
- if (qname.isEmpty()) {
- return;
- }
-
- if (bodyDepth > 0 || inPrivateMember) {
- implementationDeps.add(qname);
- } else {
- signatureDeps.add(qname);
- }
- }
-
- private TypeElement enclosingType(Element element) {
- Element enclosing = element.getEnclosingElement();
- return enclosing instanceof TypeElement te ? te : null;
- }
-
- private Element resolveElement(TreePath path) {
- try {
- return trees.getElement(path);
- } catch (IllegalArgumentException | NullPointerException e) {
- // javac may throw for synthetic elements, error types, or unresolved symbols
- return null;
- }
- }
-}
diff --git a/src/main/java/org/apache/maven/plugin/compiler/incremental/Sha256.java b/src/main/java/org/apache/maven/plugin/compiler/incremental/Sha256.java
index be37c2bc..4241b391 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/incremental/Sha256.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/incremental/Sha256.java
@@ -25,7 +25,7 @@ import java.security.NoSuchAlgorithmException;
/**
* SHA-256 hashing utility shared by the bytecode analyzer implementations.
*/
-final class Sha256 {
+public final class Sha256 {
private Sha256() {}
@@ -35,7 +35,7 @@ final class Sha256 {
* @param input the string to hash
* @return 16-character hex string
*/
- static String hash(String input) {
+ public static String hash(String input) {
return hash(input.getBytes(StandardCharsets.UTF_8));
}
@@ -45,7 +45,7 @@ final class Sha256 {
* @param content the bytes to hash
* @return 16-character hex string
*/
- static String hash(byte[] content) {
+ public static String hash(byte[] content) {
try {
var md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(content);
diff --git a/src/main/java24/org/apache/maven/plugin/compiler/incremental/ClassfileClassAnalyzer.java b/src/main/java24/org/apache/maven/plugin/compiler/incremental/ClassfileClassAnalyzer.java
index 33eb1c91..3e9ff621 100644
--- a/src/main/java24/org/apache/maven/plugin/compiler/incremental/ClassfileClassAnalyzer.java
+++ b/src/main/java24/org/apache/maven/plugin/compiler/incremental/ClassfileClassAnalyzer.java
@@ -23,6 +23,9 @@ import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.FieldModel;
import java.lang.classfile.MethodModel;
+import java.lang.classfile.attribute.ModuleAttribute;
+import java.lang.classfile.attribute.ModuleProvideInfo;
+import java.lang.classfile.attribute.ModuleRequireInfo;
import java.lang.classfile.instruction.FieldInstruction;
import java.lang.classfile.instruction.InvokeInstruction;
import java.lang.classfile.instruction.NewMultiArrayInstruction;
@@ -43,13 +46,22 @@ import java.util.TreeSet;
* {@code --release 24} to avoid a compile-time dependency on the classfile API
* in the main sources.
*
- * <p>The analysis is functionally equivalent to {@link AsmClassAnalyzer}: it
- * collects the same type references (class hierarchy, field/method descriptors,
- * exception types, annotations, and bytecode instructions) and produces the
- * same ABI canonical form and fingerprint.
+ * <p>Type references are classified into two sets:
+ * <ul>
+ * <li><b>signatureTypes</b> — types from the public API surface: supertype, interfaces,
+ * field/method descriptors of non-private members, exception types, annotations.
+ * These are what downstream consumers structurally depend on.</li>
+ * <li><b>implementationTypes</b> — types referenced only in method body bytecode
+ * instructions (INVOKE*, field access, NEW, CHECKCAST, etc.), and descriptor
+ * types of private members. Changes to these do not cascade to the class's
+ * signature consumers.</li>
+ * </ul>
+ *
+ * <p>{@code module-info.class} is handled specially: its ABI fingerprint is derived
+ * from the {@code Module} attribute directives (requires, exports, opens, uses,
+ * provides), and no implementation types are collected.
*
* @see BytecodeAnalyzer
- * @see AsmClassAnalyzer
* @see ClassAnalyzer
*/
class ClassfileClassAnalyzer extends ClassAnalyzer {
@@ -58,77 +70,273 @@ class ClassfileClassAnalyzer extends ClassAnalyzer {
public BytecodeAnalyzer.ClassAnalysis analyze(byte[] classBytes) {
ClassModel cm = ClassFile.of().parse(classBytes);
+ // module-info.class has the MODULE access flag
+ if (cm.flags().has(AccessFlag.MODULE)) {
+ return analyzeModuleInfo(cm);
+ }
+
String className = BytecodeAnalyzer.toJavaName(cm.thisClass().asInternalName());
- Set<String> referencedTypes = new TreeSet<>();
+ Set<String> signatureTypes = new TreeSet<>();
+ Set<String> implementationTypes = new TreeSet<>();
+ Set<String> annotationTypes = new TreeSet<>();
- collectReferencedTypes(cm, referencedTypes);
- referencedTypes.remove(className);
+ collectTypes(cm, signatureTypes, implementationTypes, annotationTypes);
+
+ // Remove self-references and JDK types
+ signatureTypes.remove(className);
+ implementationTypes.remove(className);
+ implementationTypes.removeAll(signatureTypes); // sig takes precedence
+ signatureTypes.removeIf(ClassfileClassAnalyzer::isJdkType);
+ implementationTypes.removeIf(ClassfileClassAnalyzer::isJdkType);
+ annotationTypes.removeIf(ClassfileClassAnalyzer::isJdkType);
String abiCanonical = buildCanonical(cm);
String abiFingerprint = Sha256.hash(abiCanonical);
- return new BytecodeAnalyzer.ClassAnalysis(className, abiFingerprint, abiCanonical, referencedTypes);
+ // Read the SourceFile attribute for accurate source-file attribution
+ String sourceFileName = cm.findAttribute(Attributes.sourceFile())
+ .map(sf -> sf.sourceFile().stringValue())
+ .orElse("");
+
+ return new BytecodeAnalyzer.ClassAnalysis(
+ className,
+ abiFingerprint,
+ abiCanonical,
+ signatureTypes,
+ implementationTypes,
+ annotationTypes,
+ /* moduleName= */ "",
+ /* isModuleInfo= */ false,
+ sourceFileName);
+ }
+
+ // --- module-info handling ---
+
+ private static BytecodeAnalyzer.ClassAnalysis analyzeModuleInfo(ClassModel cm) {
+ var moduleAttrOpt = cm.findAttribute(Attributes.module());
+ if (moduleAttrOpt.isEmpty()) {
+ return new BytecodeAnalyzer.ClassAnalysis(
+ "module-info", "", "", Set.of(), Set.of(), Set.of(), "", true, "module-info.java");
+ }
+
+ ModuleAttribute mod = moduleAttrOpt.get();
+ String moduleName = mod.moduleName().name().stringValue();
+ boolean isOpen = cm.flags().has(AccessFlag.OPEN);
+
+ var requires = new TreeSet<String>();
+ var exports = new TreeSet<String>();
+ var opens = new TreeSet<String>();
+ var uses = new TreeSet<String>();
+ var provides = new TreeSet<String>();
+
+ for (ModuleRequireInfo req : mod.requires()) {
+ var sb = new StringBuilder("requires ");
+ if (req.requiresFlags().contains(AccessFlag.TRANSITIVE)) sb.append("transitive ");
+ if (req.requiresFlags().contains(AccessFlag.STATIC_PHASE)) sb.append("static ");
+ sb.append(req.requires().name().stringValue());
+ requires.add(sb.toString());
+ }
+ for (var exp : mod.exports()) {
+ var sb = new StringBuilder("exports ")
+ .append(exp.exportedPackage().name().stringValue().replace('/', '.'));
+ var tos = exp.exportsTo();
+ if (!tos.isEmpty()) {
+ sb.append(" to ")
+ .append(tos.stream()
+ .map(e -> e.name().stringValue())
+ .sorted()
+ .reduce((a, b) -> a + ", " + b)
+ .orElse(""));
+ }
+ exports.add(sb.toString());
+ }
+ for (var op : mod.opens()) {
+ var sb = new StringBuilder("opens ")
+ .append(op.openedPackage().name().stringValue().replace('/', '.'));
+ var tos = op.opensTo();
+ if (!tos.isEmpty()) {
+ sb.append(" to ")
+ .append(tos.stream()
+ .map(e -> e.name().stringValue())
+ .sorted()
+ .reduce((a, b) -> a + ", " + b)
+ .orElse(""));
+ }
+ opens.add(sb.toString());
+ }
+ for (var u : mod.uses()) {
+ uses.add("uses " + BytecodeAnalyzer.toJavaName(u.asInternalName()));
+ }
+ for (ModuleProvideInfo p : mod.provides()) {
+ var sb = new StringBuilder("provides ")
+ .append(BytecodeAnalyzer.toJavaName(p.provides().asInternalName()));
+ var impls = p.providesWith();
+ if (!impls.isEmpty()) {
+ sb.append(" with ")
+ .append(impls.stream()
+ .map(i -> BytecodeAnalyzer.toJavaName(i.asInternalName()))
+ .sorted()
+ .reduce((a, b) -> a + ", " + b)
+ .orElse(""));
+ }
+ provides.add(sb.toString());
+ }
+
+ var canonical = new StringBuilder();
+ if (isOpen) canonical.append("open ");
+ canonical.append("module ").append(moduleName).append('\n');
+ for (String r : requires) canonical.append(" ").append(r).append('\n');
+ for (String e : exports) canonical.append(" ").append(e).append('\n');
+ for (String o : opens) canonical.append(" ").append(o).append('\n');
+ for (String u : uses) canonical.append(" ").append(u).append('\n');
+ for (String p : provides) canonical.append(" ").append(p).append('\n');
+
+ String abiCanonical = canonical.toString();
+ String abiFingerprint = Sha256.hash(abiCanonical);
+
+ // Signature deps for module-info: service types from uses/provides
+ var sigTypes = new TreeSet<String>();
+ for (var u : mod.uses()) {
+ String name = BytecodeAnalyzer.toJavaName(u.asInternalName());
+ if (!isJdkType(name)) sigTypes.add(name);
+ }
+ for (ModuleProvideInfo p : mod.provides()) {
+ String svc = BytecodeAnalyzer.toJavaName(p.provides().asInternalName());
+ if (!isJdkType(svc)) sigTypes.add(svc);
+ for (var impl : p.providesWith()) {
+ String implName = BytecodeAnalyzer.toJavaName(impl.asInternalName());
+ if (!isJdkType(implName)) sigTypes.add(implName);
+ }
+ }
+
+ // module-info qualified name uses the MODULE_PREFIX convention
+ String qualifiedName = AbiIncrementalBuild.MODULE_PREFIX + moduleName;
+
+ return new BytecodeAnalyzer.ClassAnalysis(
+ qualifiedName,
+ abiFingerprint,
+ abiCanonical,
+ sigTypes,
+ Set.of(),
+ Set.of(),
+ moduleName,
+ /* isModuleInfo= */ true,
+ /* sourceFileName= */ "module-info.java");
}
// --- type reference collection ---
- private static void collectReferencedTypes(ClassModel cm, Set<String> referencedTypes) {
- // superclass
- cm.superclass().ifPresent(sup -> addRef(sup.asInternalName(), referencedTypes));
+ private static void collectTypes(
+ ClassModel cm, Set<String> signatureTypes, Set<String> implementationTypes, Set<String> annotationTypes) {
- // interfaces
+ // Superclass and interfaces are always signature-level
+ cm.superclass().ifPresent(sup -> addRef(sup.asInternalName(), signatureTypes));
for (var iface : cm.interfaces()) {
- addRef(iface.asInternalName(), referencedTypes);
+ addRef(iface.asInternalName(), signatureTypes);
}
- // annotations (visible and invisible)
- cm.findAttribute(Attributes.runtimeVisibleAnnotations())
- .ifPresent(a -> a.annotations()
- .forEach(ann -> addDescriptor(ann.className().stringValue(), referencedTypes)));
- cm.findAttribute(Attributes.runtimeInvisibleAnnotations())
- .ifPresent(a -> a.annotations()
- .forEach(ann -> addDescriptor(ann.className().stringValue(), referencedTypes)));
+ // Class-level annotations → signature + annotation tracking
+ collectAnnotations(
+ cm.findAttribute(Attributes.runtimeVisibleAnnotations())
+ .map(a -> a.annotations())
+ .orElse(List.of()),
+ signatureTypes,
+ annotationTypes);
+ collectAnnotations(
+ cm.findAttribute(Attributes.runtimeInvisibleAnnotations())
+ .map(a -> a.annotations())
+ .orElse(List.of()),
+ signatureTypes,
+ annotationTypes);
- // fields: descriptor types + field annotations
for (FieldModel field : cm.fields()) {
- addDescriptor(field.fieldType().stringValue(), referencedTypes);
- field.findAttribute(Attributes.runtimeVisibleAnnotations())
- .ifPresent(a -> a.annotations()
- .forEach(ann -> addDescriptor(ann.className().stringValue(), referencedTypes)));
- field.findAttribute(Attributes.runtimeInvisibleAnnotations())
- .ifPresent(a -> a.annotations()
- .forEach(ann -> addDescriptor(ann.className().stringValue(), referencedTypes)));
+ boolean isPrivate = field.flags().has(AccessFlag.PRIVATE);
+ Set<String> descTarget = isPrivate ? implementationTypes : signatureTypes;
+
+ addDescriptor(field.fieldType().stringValue(), descTarget);
+
+ // Field annotations go to the same target as the field descriptor
+ collectAnnotations(
+ field.findAttribute(Attributes.runtimeVisibleAnnotations())
+ .map(a -> a.annotations())
+ .orElse(List.of()),
+ descTarget,
+ annotationTypes);
+ collectAnnotations(
+ field.findAttribute(Attributes.runtimeInvisibleAnnotations())
+ .map(a -> a.annotations())
+ .orElse(List.of()),
+ descTarget,
+ annotationTypes);
}
- // methods: descriptor types, exception types, code instructions, method annotations
for (MethodModel method : cm.methods()) {
- String methodDesc = method.methodType().stringValue();
- addMethodDescriptor(methodDesc, referencedTypes);
+ boolean isPrivate = method.flags().has(AccessFlag.PRIVATE);
+ Set<String> sigTarget = isPrivate ? implementationTypes : signatureTypes;
+ // Method descriptor types (params + return) and exceptions → sig if non-private
+ addMethodDescriptor(method.methodType().stringValue(), sigTarget);
method.findAttribute(Attributes.exceptions())
- .ifPresent(ex -> ex.exceptions().forEach(e -> addRef(e.asInternalName(), referencedTypes)));
+ .ifPresent(ex -> ex.exceptions().forEach(e -> addRef(e.asInternalName(), sigTarget)));
- method.findAttribute(Attributes.runtimeVisibleAnnotations())
- .ifPresent(a -> a.annotations()
- .forEach(ann -> addDescriptor(ann.className().stringValue(), referencedTypes)));
- method.findAttribute(Attributes.runtimeInvisibleAnnotations())
- .ifPresent(a -> a.annotations()
- .forEach(ann -> addDescriptor(ann.className().stringValue(), referencedTypes)));
+ // Method annotations → same target as descriptor
+ collectAnnotations(
+ method.findAttribute(Attributes.runtimeVisibleAnnotations())
+ .map(a -> a.annotations())
+ .orElse(List.of()),
+ sigTarget,
+ annotationTypes);
+ collectAnnotations(
+ method.findAttribute(Attributes.runtimeInvisibleAnnotations())
+ .map(a -> a.annotations())
+ .orElse(List.of()),
+ sigTarget,
+ annotationTypes);
+ // Method body instructions → always implementation-level
method.code().ifPresent(code -> {
for (var element : code) {
switch (element) {
- case InvokeInstruction ii -> addRef(ii.owner().asInternalName(), referencedTypes);
- case FieldInstruction fi -> addRef(fi.owner().asInternalName(), referencedTypes);
- case TypeCheckInstruction tci -> addRef(tci.type().asInternalName(), referencedTypes);
- case NewObjectInstruction noi -> addRef(noi.className().asInternalName(), referencedTypes);
+ case InvokeInstruction ii -> addRef(ii.owner().asInternalName(), implementationTypes);
+ case FieldInstruction fi -> addRef(fi.owner().asInternalName(), implementationTypes);
+ case TypeCheckInstruction tci -> addRef(tci.type().asInternalName(), implementationTypes);
+ case NewObjectInstruction noi -> addRef(noi.className().asInternalName(), implementationTypes);
case NewMultiArrayInstruction nma ->
- addRef(nma.arrayType().asInternalName(), referencedTypes);
+ addRef(nma.arrayType().asInternalName(), implementationTypes);
default -> {}
}
}
});
}
+
+ // Scan the constant pool for class references that are not captured by instructions.
+ // This covers compile-time constants (static final primitives) whose values are inlined
+ // by javac — the referencing class has no GETSTATIC instruction, but the resolved class
+ // is still present in the constant pool.
+ for (var entry : cm.constantPool()) {
+ if (entry instanceof java.lang.classfile.constantpool.ClassEntry ce) {
+ String internalName = ce.asInternalName();
+ // Skip array type descriptors and the class itself
+ if (!internalName.startsWith("[")
+ && !internalName.equals(cm.thisClass().asInternalName())) {
+ addRef(internalName, implementationTypes);
+ }
+ }
+ }
+ }
+
+ private static void collectAnnotations(
+ java.util.List<? extends java.lang.classfile.Annotation> annotations,
+ Set<String> typeTarget,
+ Set<String> annotationTarget) {
+ for (var ann : annotations) {
+ String descriptor = ann.className().stringValue();
+ String name = descriptorToJavaName(descriptor);
+ if (name != null) {
+ typeTarget.add(name);
+ annotationTarget.add(name);
+ }
+ }
}
// --- ABI canonical form ---
@@ -183,6 +391,13 @@ class ClassfileClassAnalyzer extends ClassAnalyzer {
// --- utilities ---
+ private static boolean isJdkType(String name) {
+ return name.startsWith("java.")
+ || name.startsWith("javax.")
+ || name.startsWith("jdk.")
+ || name.startsWith("sun.");
+ }
+
private static int accessMask(Set<AccessFlag> flags) {
int mask = 0;
for (AccessFlag flag : flags) {
@@ -191,25 +406,34 @@ class ClassfileClassAnalyzer extends ClassAnalyzer {
return mask;
}
- private static void addRef(String internalName, Set<String> referencedTypes) {
+ private static void addRef(String internalName, Set<String> types) {
String resolved = resolveInternalName(internalName);
if (resolved != null) {
- referencedTypes.add(resolved);
+ types.add(resolved);
}
}
- private static void addDescriptor(String descriptor, Set<String> referencedTypes) {
+ /** Converts a field type descriptor ({@code Lcom/Foo;}, {@code [Lcom/Foo;}) to a Java name, or null for primitives. */
+ private static String descriptorToJavaName(String descriptor) {
int i = 0;
while (i < descriptor.length() && descriptor.charAt(i) == '[') {
i++;
}
String core = descriptor.substring(i);
if (core.startsWith("L") && core.endsWith(";")) {
- addRef(core.substring(1, core.length() - 1), referencedTypes);
+ return BytecodeAnalyzer.toJavaName(core.substring(1, core.length() - 1));
+ }
+ return null; // primitive or array-of-primitive
+ }
+
+ private static void addDescriptor(String descriptor, Set<String> types) {
+ String name = descriptorToJavaName(descriptor);
+ if (name != null) {
+ types.add(name);
}
}
- private static void addMethodDescriptor(String methodDesc, Set<String> referencedTypes) {
+ private static void addMethodDescriptor(String methodDesc, Set<String> types) {
int close = methodDesc.indexOf(')');
int i = 1;
while (i < close) {
@@ -219,13 +443,13 @@ class ClassfileClassAnalyzer extends ClassAnalyzer {
if (i < close) {
if (methodDesc.charAt(i) == 'L') {
int end = methodDesc.indexOf(';', i);
- addRef(methodDesc.substring(i + 1, end), referencedTypes);
+ types.add(BytecodeAnalyzer.toJavaName(methodDesc.substring(i + 1, end)));
i = end + 1;
} else {
i++;
}
}
}
- addDescriptor(methodDesc.substring(close + 1), referencedTypes);
+ addDescriptor(methodDesc.substring(close + 1), types);
}
}
diff --git a/src/test/java/org/apache/maven/plugin/compiler/incremental/AbiExtractorTest.java b/src/test/java/org/apache/maven/plugin/compiler/incremental/AbiExtractorTest.java
deleted file mode 100644
index 75557ad1..00000000
--- a/src/test/java/org/apache/maven/plugin/compiler/incremental/AbiExtractorTest.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.plugin.compiler.incremental;
-
-import java.nio.file.Path;
-import java.util.Map;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-
-class AbiExtractorTest {
-
- @TempDir
- Path sourceDir;
-
- @TempDir
- Path outputDir;
-
- private String fingerprintFor(String source) throws Exception {
- CompilerTestHelper.writeSource(sourceDir, "test", "Subject", source);
- Map<String, SourceFileAnalysis> results = CompilerTestHelper.compileAndAnalyze(sourceDir, outputDir);
- return results.get("test.Subject").abiFingerprint();
- }
-
- @Test
- void bodyOnlyChangeProducesSameFingerprint() throws Exception {
- String fp1 = fingerprintFor("package test; public class Subject { public int value() { return 1; } }");
- // Reset dirs for a clean compile
- sourceDir = sourceDir.resolveSibling("src2");
- outputDir = outputDir.resolveSibling("out2");
- String fp2 = fingerprintFor("package test; public class Subject { public int value() { return 42; } }");
- assertEquals(fp1, fp2, "Body-only change should not affect ABI fingerprint");
- }
-
- @Test
- void addingPublicMethodChangesFingerprint() throws Exception {
- String fp1 = fingerprintFor("package test; public class Subject { public int value() { return 1; } }");
- sourceDir = sourceDir.resolveSibling("src2");
- outputDir = outputDir.resolveSibling("out2");
- String fp2 = fingerprintFor(
- "package test; public class Subject { public int value() { return 1; } public String name() { return \"\"; } }");
- assertNotEquals(fp1, fp2, "Adding public method should change ABI fingerprint");
- }
-
- @Test
- void addingPrivateMethodDoesNotChangeFingerprint() throws Exception {
- String fp1 = fingerprintFor("package test; public class Subject { public int value() { return 1; } }");
- sourceDir = sourceDir.resolveSibling("src2");
- outputDir = outputDir.resolveSibling("out2");
- String fp2 = fingerprintFor(
- "package test; public class Subject { public int value() { return helper(); } private int helper() { return 1; } }");
- assertEquals(fp1, fp2, "Adding private method should not change ABI fingerprint");
- }
-
- @Test
- void changingConstantValueChangesFingerprint() throws Exception {
- String fp1 = fingerprintFor("package test; public class Subject { public static final int VALUE = 100; }");
- sourceDir = sourceDir.resolveSibling("src2");
- outputDir = outputDir.resolveSibling("out2");
- String fp2 = fingerprintFor("package test; public class Subject { public static final int VALUE = 200; }");
- assertNotEquals(fp1, fp2, "Changing constant value should change ABI fingerprint");
- }
-
- @Test
- void genericTypeParametersIncluded() throws Exception {
- String fp1 = fingerprintFor("package test; public class Subject<T> { public T get() { return null; } }");
- sourceDir = sourceDir.resolveSibling("src2");
- outputDir = outputDir.resolveSibling("out2");
- String fp2 = fingerprintFor(
- "package test; public class Subject<T extends Comparable<T>> { public T get() { return null; } }");
- assertNotEquals(fp1, fp2, "Changing type parameter bounds should change ABI fingerprint");
- }
-
- @Test
- void changingThrowsClauseChangesFingerprint() throws Exception {
- String fp1 = fingerprintFor("package test; public class Subject { public void run() { } }");
- sourceDir = sourceDir.resolveSibling("src2");
- outputDir = outputDir.resolveSibling("out2");
- String fp2 = fingerprintFor("package test; public class Subject { public void run() throws Exception { } }");
- assertNotEquals(fp1, fp2, "Changing throws clause should change ABI fingerprint");
- }
-
- @Test
- void changingSuperclassChangesFingerprint() throws Exception {
- CompilerTestHelper.writeSource(sourceDir, "test", "Base", "package test; public class Base { }");
- CompilerTestHelper.writeSource(sourceDir, "test", "Subject", "package test; public class Subject { }");
- Map<String, SourceFileAnalysis> r1 = CompilerTestHelper.compileAndAnalyze(sourceDir, outputDir);
- String fp1 = r1.get("test.Subject").abiFingerprint();
-
- Path sourceDir2 = sourceDir.resolveSibling("src2");
- Path outputDir2 = outputDir.resolveSibling("out2");
- CompilerTestHelper.writeSource(sourceDir2, "test", "Base", "package test; public class Base { }");
- CompilerTestHelper.writeSource(
- sourceDir2, "test", "Subject", "package test; public class Subject extends Base { }");
- Map<String, SourceFileAnalysis> r2 = CompilerTestHelper.compileAndAnalyze(sourceDir2, outputDir2);
- String fp2 = r2.get("test.Subject").abiFingerprint();
-
- assertNotEquals(fp1, fp2, "Changing superclass should change ABI fingerprint");
- }
-
- @Test
- void sealedClassPermitsAffectsFingerprint() throws Exception {
- Path src1 = sourceDir.resolveSibling("srcSealed1");
- Path out1 = outputDir.resolveSibling("outSealed1");
- CompilerTestHelper.writeSource(
- src1, "test", "Subject", "package test; public sealed class Subject permits A { }");
- CompilerTestHelper.writeSource(src1, "test", "A", "package test; public final class A extends Subject { }");
- Map<String, SourceFileAnalysis> r1 = CompilerTestHelper.compileAndAnalyze(src1, out1);
- String fp1 = r1.get("test.Subject").abiFingerprint();
-
- Path src2 = sourceDir.resolveSibling("srcSealed2");
- Path out2 = outputDir.resolveSibling("outSealed2");
- CompilerTestHelper.writeSource(
- src2, "test", "Subject", "package test; public sealed class Subject permits A, B { }");
- CompilerTestHelper.writeSource(src2, "test", "A", "package test; public final class A extends Subject { }");
- CompilerTestHelper.writeSource(src2, "test", "B", "package test; public final class B extends Subject { }");
- Map<String, SourceFileAnalysis> r2 = CompilerTestHelper.compileAndAnalyze(src2, out2);
- String fp2 = r2.get("test.Subject").abiFingerprint();
-
- assertNotEquals(fp1, fp2, "Adding a permitted subclass should change ABI fingerprint");
- }
-
- @Test
- void enumConstantReorderingAffectsFingerprint() throws Exception {
- String fp1 = fingerprintFor("package test; public enum Subject { RED, GREEN, BLUE }");
- sourceDir = sourceDir.resolveSibling("srcEnum2");
- outputDir = outputDir.resolveSibling("outEnum2");
- String fp2 = fingerprintFor("package test; public enum Subject { BLUE, GREEN, RED }");
- assertNotEquals(fp1, fp2, "Reordering enum constants should change ABI fingerprint (ordinal changes)");
- }
-}
diff --git a/src/test/java/org/apache/maven/plugin/compiler/incremental/BytecodeAnalyzerTest.java b/src/test/java/org/apache/maven/plugin/compiler/incremental/BytecodeAnalyzerTest.java
index 0c0de88a..7d007037 100644
--- a/src/test/java/org/apache/maven/plugin/compiler/incremental/BytecodeAnalyzerTest.java
+++ b/src/test/java/org/apache/maven/plugin/compiler/incremental/BytecodeAnalyzerTest.java
@@ -146,7 +146,8 @@ class BytecodeAnalyzerTest {
assertEquals(fromPath.className(), fromBytes.className());
assertEquals(fromPath.abiFingerprint(), fromBytes.abiFingerprint());
assertEquals(fromPath.abiCanonical(), fromBytes.abiCanonical());
- assertEquals(fromPath.referencedTypes(), fromBytes.referencedTypes());
+ assertEquals(fromPath.signatureTypes(), fromBytes.signatureTypes());
+ assertEquals(fromPath.implementationTypes(), fromBytes.implementationTypes());
}
@Test
@@ -209,43 +210,38 @@ class BytecodeAnalyzerTest {
}
/**
- * When running on JDK 24+, {@link BytecodeAnalyzer} uses the {@code ClassfileClassAnalyzer}
- * instead of {@link AsmClassAnalyzer}. Both must produce identical results for the same
- * class file. This test cross-validates the two implementations against each other.
- *
- * <p>The {@code ClassfileClassAnalyzer} class is compiled with {@code --release 24} and
- * only exists in the output directory when building on JDK 24+. To avoid a compile-time
- * dependency on a class that may not exist, the analyzer is instantiated reflectively.
+ * When running on JDK 24+, {@link BytecodeAnalyzer} uses the {@code ClassfileClassAnalyzer}.
+ * Verifies idempotency: analyzing the same bytes twice produces identical results.
+ * Also verifies that sig/impl split is non-trivially populated for a class with
+ * user-defined type cross-references.
*/
@Test
@org.junit.jupiter.api.condition.EnabledForJreRange(min = org.junit.jupiter.api.condition.JRE.JAVA_24)
- void asmAndClassfileAnalyzersProduceIdenticalResults() throws Exception {
+ void classfileAnalyzerIsIdempotent() throws Exception {
+ // Write two classes so Subject references UserType (a non-JDK type → sig dependency)
+ CompilerTestHelper.writeSource(sourceDir, "test", "UserType", "package test; public class UserType {}");
CompilerTestHelper.writeSource(sourceDir, "test", "Subject", """
package test;
public class Subject implements Runnable {
public static final String CONST = "hello";
private int secret = 42;
- public String greet(int x, java.util.List<String> items) { return CONST + x; }
- @Override public void run() { greet(1, null); }
- protected static int helper(byte b) throws java.io.IOException { return b; }
+ public UserType getUser() { return new UserType(); }
+ @Override public void run() { getUser(); }
}
""");
CompilerTestHelper.compileAndAnalyze(sourceDir, outputDir);
byte[] bytes = Files.readAllBytes(outputDir.resolve("test/Subject.class"));
- var asmResult = new AsmClassAnalyzer().analyze(bytes);
+ var r1 = BytecodeAnalyzer.analyze(bytes);
+ var r2 = BytecodeAnalyzer.analyze(bytes);
- // Instantiate ClassfileClassAnalyzer reflectively — the class is only compiled on JDK 24+
- ClassAnalyzer cfAnalyzer =
- (ClassAnalyzer) Class.forName("org.apache.maven.plugin.compiler.incremental.ClassfileClassAnalyzer")
- .getDeclaredConstructor()
- .newInstance();
- var cfResult = cfAnalyzer.analyze(bytes);
-
- assertEquals(asmResult.className(), cfResult.className(), "className");
- assertEquals(asmResult.abiCanonical(), cfResult.abiCanonical(), "abiCanonical");
- assertEquals(asmResult.abiFingerprint(), cfResult.abiFingerprint(), "abiFingerprint");
- assertEquals(asmResult.referencedTypes(), cfResult.referencedTypes(), "referencedTypes");
+ assertEquals(r1.className(), r2.className(), "className");
+ assertEquals(r1.abiCanonical(), r2.abiCanonical(), "abiCanonical");
+ assertEquals(r1.abiFingerprint(), r2.abiFingerprint(), "abiFingerprint");
+ assertEquals(r1.signatureTypes(), r2.signatureTypes(), "signatureTypes");
+ assertEquals(r1.implementationTypes(), r2.implementationTypes(), "implementationTypes");
+ // Subject.getUser() returns UserType → UserType is a non-JDK signature dependency
+ assertTrue(r1.signatureTypes().contains("test.UserType"), "test.UserType should be a signature type ref");
}
}
diff --git a/src/test/java/org/apache/maven/plugin/compiler/incremental/CompilerTestHelper.java b/src/test/java/org/apache/maven/plugin/compiler/incremental/CompilerTestHelper.java
index b6eb91b4..b39f64ae 100644
--- a/src/test/java/org/apache/maven/plugin/compiler/incremental/CompilerTestHelper.java
+++ b/src/test/java/org/apache/maven/plugin/compiler/incremental/CompilerTestHelper.java
@@ -29,8 +29,6 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
-import com.sun.source.util.JavacTask;
-
/**
* Shared helper for incremental compilation tests.
*/
@@ -42,6 +40,10 @@ class CompilerTestHelper {
Files.writeString(packageDir.resolve(className + ".java"), source);
}
+ /**
+ * Compiles all {@code .java} files under {@code sourceDir} into {@code outputDir}
+ * and returns the output directory. No ABI analysis — raw javac only.
+ */
static Map<String, SourceFileAnalysis> compileAndAnalyze(Path sourceDir, Path outputDir) throws IOException {
Files.createDirectories(outputDir);
List<Path> sourceFiles;
@@ -54,16 +56,38 @@ class CompilerTestHelper {
try (var fm = compiler.getStandardFileManager(null, null, null)) {
fm.setLocation(StandardLocation.CLASS_OUTPUT, List.of(outputDir.toFile()));
var units = fm.getJavaFileObjectsFromPaths(sourceFiles);
- var task = (JavacTask) compiler.getTask(null, fm, null, null, null, units);
-
- var analyzer = new CompilationAnalyzer(task);
- task.addTaskListener(analyzer);
+ var task = compiler.getTask(null, fm, null, null, null, units);
if (!task.call()) {
throw new RuntimeException("Compilation failed");
}
- return analyzer.getResults();
+ // Build a minimal SourceFileAnalysis map from bytecode (no dep graph needed here)
+ Map<String, SourceFileAnalysis> results = new java.util.LinkedHashMap<>();
+ for (Path sf : sourceFiles) {
+ try (Stream<Path> walk = Files.walk(outputDir)) {
+ walk.filter(p -> p.toString().endsWith(".class")).forEach(cf -> {
+ try {
+ var analysis = BytecodeAnalyzer.analyze(cf);
+ var sfa = new SourceFileAnalysis(
+ analysis.className(),
+ sf.toString(),
+ analysis.signatureTypes(),
+ analysis.implementationTypes(),
+ analysis.abiFingerprint(),
+ analysis.abiCanonical(),
+ analysis.annotationTypes(),
+ analysis.moduleName());
+ results.put(analysis.className(), sfa);
+ } catch (IOException e) {
+ // best effort
+ }
+ });
+ } catch (IOException e) {
+ // best effort
+ }
+ }
+ return results;
}
}
diff --git a/src/test/java/org/apache/maven/plugin/compiler/incremental/DependencyScannerTest.java b/src/test/java/org/apache/maven/plugin/compiler/incremental/DependencyScannerTest.java
deleted file mode 100644
index ee4d0d53..00000000
--- a/src/test/java/org/apache/maven/plugin/compiler/incremental/DependencyScannerTest.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.plugin.compiler.incremental;
-
-import java.nio.file.Path;
-import java.util.Map;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class DependencyScannerTest {
-
- @TempDir
- Path sourceDir;
-
- @TempDir
- Path outputDir;
-
- @BeforeEach
- void setUp() throws Exception {
- CompilerTestHelper.writeSource(
- sourceDir, "dep", "Model", "package dep; public class Model { public String name() { return \"\"; } }");
- CompilerTestHelper.writeSource(
- sourceDir,
- "dep",
- "MyException",
- "package dep; public class MyException extends Exception { public MyException(String msg) { super(msg); } }");
- CompilerTestHelper.writeSource(
- sourceDir, "dep", "Helper", "package dep; public class Helper { public void help() {} }");
- }
-
- private SourceFileAnalysis analyze(String source) throws Exception {
- CompilerTestHelper.writeSource(sourceDir, "test", "Subject", source);
- Map<String, SourceFileAnalysis> results = CompilerTestHelper.compileAndAnalyze(sourceDir, outputDir);
- return results.get("test.Subject");
- }
-
- @Test
- void extendsIsSignatureDep() throws Exception {
- var a = analyze("package test; import dep.Model; public class Subject extends Model { }");
- assertTrue(a.signatureDeps().contains("dep.Model"), "extends type should be signature dep");
- }
-
- @Test
- void implementsIsSignatureDep() throws Exception {
- CompilerTestHelper.writeSource(
- sourceDir, "dep", "Iface", "package dep; public interface Iface { void run(); }");
- var a = analyze(
- "package test; import dep.Iface; public class Subject implements Iface { public void run() {} }");
- assertTrue(a.signatureDeps().contains("dep.Iface"), "implements type should be signature dep");
- }
-
- @Test
- void returnTypeIsSignatureDep() throws Exception {
- var a = analyze("package test; import dep.Model; public class Subject { public Model get() { return null; } }");
- assertTrue(a.signatureDeps().contains("dep.Model"), "return type should be signature dep");
- }
-
- @Test
- void parameterTypeIsSignatureDep() throws Exception {
- var a = analyze("package test; import dep.Model; public class Subject { public void set(Model m) { } }");
- assertTrue(a.signatureDeps().contains("dep.Model"), "parameter type should be signature dep");
- }
-
- @Test
- void throwsTypeIsSignatureDep() throws Exception {
- var a = analyze(
- "package test; import dep.MyException; public class Subject { public void run() throws MyException { } }");
- assertTrue(a.signatureDeps().contains("dep.MyException"), "throws type should be signature dep");
- }
-
- @Test
- void bodyOnlyUsageIsImplementationDep() throws Exception {
- var a = analyze(
- "package test; import dep.Helper; public class Subject { public void run() { new Helper().help(); } }");
- assertTrue(a.implementationDeps().contains("dep.Helper"), "body-only usage should be impl dep");
- assertFalse(a.signatureDeps().contains("dep.Helper"), "body-only usage should not be sig dep");
- }
-
- @Test
- void privateFieldIsImplementationDep() throws Exception {
- var a = analyze(
- "package test; import dep.Helper; public class Subject { private Helper helper = new Helper(); }");
- assertTrue(a.implementationDeps().contains("dep.Helper"), "private field type should be impl dep");
- assertFalse(a.signatureDeps().contains("dep.Helper"), "private field type should not be sig dep");
- }
-
- @Test
- void publicFieldTypeIsSignatureDep() throws Exception {
- var a = analyze("package test; import dep.Model; public class Subject { public Model model; }");
- assertTrue(a.signatureDeps().contains("dep.Model"), "public field type should be signature dep");
- }
-
- @Test
- void importDoesNotCreateFalseSignatureDep() throws Exception {
- var a = analyze(
- "package test; import dep.Helper; public class Subject { public void run() { new Helper().help(); } }");
- assertFalse(a.signatureDeps().contains("dep.Helper"), "import should not create false signature dep");
- assertTrue(a.implementationDeps().contains("dep.Helper"), "actual usage in body should be impl dep");
- }
-
- @Test
- void fieldInitializerIsImplementationDep() throws Exception {
- var a = analyze(
- "package test; import dep.Helper; public class Subject { public String name = new Helper().toString(); }");
- assertTrue(
- a.implementationDeps().contains("dep.Helper"), "field initializer type should be implementation dep");
- }
-
- @Test
- void noSelfReferences() throws Exception {
- var a = analyze("package test; public class Subject { public Subject self() { return this; } }");
- assertFalse(a.signatureDeps().contains("test.Subject"), "should not contain self-reference");
- assertFalse(a.implementationDeps().contains("test.Subject"), "should not contain self-reference");
- }
-
- @Test
- void jdkTypesFiltered() throws Exception {
- var a = analyze("package test; public class Subject { public String name() { return \"\"; } }");
- assertFalse(a.signatureDeps().contains("java.lang.String"), "JDK types should be filtered");
- }
-}