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");
-    }
-}