From 86f0b8ad6753069220db7e8cba53aabdbdf54537 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 13 Aug 2026 13:44:49 +0200 Subject: [PATCH 1/2] Improving error messages when an annotation processor crashes. --- .../java/source/indexing/APTUtils.java | 44 ++- .../java/source/parsing/JavacParser.java | 3 +- .../java/source/indexing/APTUtilsTest.java | 254 +++++++++++++++++- 3 files changed, 286 insertions(+), 15 deletions(-) diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/indexing/APTUtils.java b/java/java.source.base/src/org/netbeans/modules/java/source/indexing/APTUtils.java index ba5871e09eb4..220c16a31019 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/indexing/APTUtils.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/indexing/APTUtils.java @@ -18,6 +18,8 @@ */ package org.netbeans.modules.java.source.indexing; +import com.sun.tools.javac.comp.Enter; +import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.util.Abort; import com.sun.tools.javac.util.ClientCodeException; import com.sun.tools.javac.util.Context; @@ -77,6 +79,7 @@ import org.netbeans.api.project.ui.OpenProjects; import org.netbeans.modules.classfile.ClassFile; import org.netbeans.modules.classfile.Module; +import org.netbeans.modules.java.source.NoJavacHelper; import org.netbeans.modules.java.source.parsing.CachingArchiveClassLoader; import org.netbeans.modules.java.source.parsing.JavacParser; import org.netbeans.modules.java.source.usages.LongHashMap; @@ -1005,18 +1008,15 @@ public void init(ProcessingEnvironment processingEnv) { throw err; } catch (Throwable t) { initFailed = true; - StringBuilder exception = new StringBuilder(); - exception.append(t.getMessage()).append("\n"); - for (StackTraceElement ste : t.getStackTrace()) { - exception.append(ste).append("\n"); - } - processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, Bundle.ERR_ProcessorException(delegate.getClass().getName(), exception.toString())); + Enter enter = Enter.instance(((JavacProcessingEnvironment) processingEnv).getContext()); + Element topLevel = enter.getEnvs().iterator().hasNext() ? enter.getEnvs().iterator().next().enclClass.sym + : null; + reportError(processingEnv, t, topLevel); } this.processingEnv = processingEnv; } @Override - @Messages("ERR_ProcessorException=Annotation processor {0} failed with an exception: {1}") public boolean process(Set annotations, RoundEnvironment roundEnv) { if (initFailed || processFailed) { return false; @@ -1029,16 +1029,34 @@ public boolean process(Set annotations, RoundEnvironment } catch (Throwable t) { processFailed = true; Element el = roundEnv.getRootElements().isEmpty() ? null : roundEnv.getRootElements().iterator().next(); - StringBuilder exception = new StringBuilder(); - exception.append(t.getMessage()).append("\n"); - for (StackTraceElement ste : t.getStackTrace()) { - exception.append(ste).append("\n"); - } - processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, Bundle.ERR_ProcessorException(delegate.getClass().getName(), exception.toString()), el); + reportError(processingEnv, t, el); return false; } } + @Messages({ + "# {0} - processor name", + "# {1} - processor exception", + "ERR_ProcessorException=Annotation processor {0} failed with an exception: {1}", + "# {0} - JDK version", + "# {1} - processor exception", + "ERR_LombokException=Lombok failed, does your version of Lombok support javac from JDK {0}? Exception: {1}" + }) + private void reportError(ProcessingEnvironment processingEnv, Throwable t, Element targetEl) { + StringBuilder exception = new StringBuilder(); + exception.append(t.getMessage()).append("\n"); + for (StackTraceElement ste : t.getStackTrace()) { + exception.append(ste).append("\n"); + } + String message; + if (delegate.getClass().getName().startsWith(JavacParser.LOMBOK_ANNOTATION_PROCESSOR_PREFIX)) { + message = Bundle.ERR_LombokException(NoJavacHelper.REQUIRED_JAVAC_VERSION, exception.toString()); + } else { + message = Bundle.ERR_ProcessorException(delegate.getClass().getName(), exception.toString()); + } + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message, targetEl); + } + @Override public Iterable getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) { if (initFailed) { diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/parsing/JavacParser.java b/java/java.source.base/src/org/netbeans/modules/java/source/parsing/JavacParser.java index 6b7483c1b31e..6635d94819b6 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/parsing/JavacParser.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/parsing/JavacParser.java @@ -156,6 +156,7 @@ public class JavacParser extends Parser { private static final boolean DISABLE_PARAMETER_NAMES_READING = Boolean.getBoolean("org.netbeans.modules.java.source.parsing.JavacParser.no_parameter_names"); //NOI18N private static final Set> HUGE_SNAPSHOTS = new HashSet<>(); private static final LowMemoryWatcher LOW_MEMORY_WATCHER = LowMemoryWatcher.getInstance(); + public static final String LOMBOK_ANNOTATION_PROCESSOR_PREFIX = "lombok."; public static final String LOMBOK_DETECTED = "lombokDetected"; /** @@ -1012,7 +1013,7 @@ private static JavacTaskImpl createJavacTask( aptEnabled = false; } else { for (Processor p : processors) { - if ("lombok.core.AnnotationProcessor".equals(p.getClass().getName())) { + if (p.getClass().getName().startsWith(LOMBOK_ANNOTATION_PROCESSOR_PREFIX)) { options.add("-XD" + LOMBOK_DETECTED); break; } diff --git a/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/indexing/APTUtilsTest.java b/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/indexing/APTUtilsTest.java index f96c0cf39326..ad165b90ecf0 100644 --- a/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/indexing/APTUtilsTest.java +++ b/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/indexing/APTUtilsTest.java @@ -21,6 +21,7 @@ import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; +import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -33,14 +34,21 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; +import java.util.stream.Stream; import javax.swing.event.ChangeListener; +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.classpath.JavaClassPathConstants; import org.netbeans.api.java.queries.AnnotationProcessingQuery; import org.netbeans.api.java.queries.AnnotationProcessingQuery.Trigger; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.TestUtilities; import org.netbeans.junit.MockServices; import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.source.NoJavacHelper; import org.netbeans.modules.parsing.api.indexing.IndexingManager; import org.netbeans.modules.parsing.impl.indexing.CacheFolder; import org.netbeans.spi.java.classpath.ClassPathFactory; @@ -176,6 +184,241 @@ public void testRefreshedIndexOnceAfterAnnotationProcessingChange() throws Inter } + public void testBrokenAP() throws Exception { + APQ.result.setAnnotationProcessingEnabled(EnumSet.of(Trigger.ON_SCAN, Trigger.IN_EDITOR), + Set.of("AnnotationProcessor")); + { + //broken init: + FileObject processorClassesDir = + compileProcessor(""" + import java.util.Set; + import javax.annotation.processing.AbstractProcessor; + import javax.annotation.processing.ProcessingEnvironment; + import javax.annotation.processing.RoundEnvironment; + import javax.annotation.processing.SupportedAnnotationTypes; + import javax.lang.model.element.TypeElement; + + @SupportedAnnotationTypes("*") + public class AnnotationProcessor extends AbstractProcessor { + + @Override + public void init(ProcessingEnvironment processingEnv) { + throw new IllegalStateException("Broken."); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + throw new IllegalStateException("Should not get here."); + } + + } + """); + try { + processorPath.add(processorClassesDir); + FileObject test = FileUtil.createData(root1, "Test.java"); + TestUtilities.copyStringToFile(test, + """ + public class Test{} + """); + JavaSource source = JavaSource.forFileObject(test); + source.runUserActionTask(cc -> { + cc.toPhase(JavaSource.Phase.RESOLVED); + List messages = + cc.getDiagnostics() + .stream() + .map(d -> d.getMessage(null)) + .toList(); + if (messages.stream().noneMatch(msg -> msg.contains(Bundle.ERR_ProcessorException("AnnotationProcessor", "Broken.")))) { + fail("Expected error not found, all messages: " + messages); + } + if (messages.stream().anyMatch(msg -> msg.contains("Should not get here"))) { + fail("Unexpected error found, all messages: " + messages); + } + }, true); + } finally { + processorPath.remove(processorClassesDir); + } + } + + { + //broken process: + FileObject processorClassesDir = + compileProcessor(""" + import java.util.Set; + import javax.annotation.processing.AbstractProcessor; + import javax.annotation.processing.ProcessingEnvironment; + import javax.annotation.processing.RoundEnvironment; + import javax.annotation.processing.SupportedAnnotationTypes; + import javax.lang.model.element.TypeElement; + + @SupportedAnnotationTypes("*") + public class AnnotationProcessor extends AbstractProcessor { + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + throw new IllegalStateException("Broken."); + } + + } + """); + try { + processorPath.add(processorClassesDir); + FileObject test = FileUtil.createData(root1, "Test.java"); + TestUtilities.copyStringToFile(test, + """ + public class Test{} + """); + JavaSource source = JavaSource.forFileObject(test); + source.runUserActionTask(cc -> { + cc.toPhase(JavaSource.Phase.RESOLVED); + List messages = + cc.getDiagnostics() + .stream() + .map(d -> d.getMessage(null)) + .toList(); + if (messages.stream().noneMatch(msg -> msg.contains(Bundle.ERR_ProcessorException("AnnotationProcessor", "Broken.")))) { + fail("Expected error not found, all messages: " + messages); + } + if (messages.stream().anyMatch(msg -> msg.contains("Should not get here"))) { + fail("Unexpected error found, all messages: " + messages); + } + }, true); + } finally { + processorPath.remove(processorClassesDir); + } + } + } + + public void testBrokenAPLombok() throws Exception { + APQ.result.setAnnotationProcessingEnabled(EnumSet.of(Trigger.ON_SCAN, Trigger.IN_EDITOR), + Set.of("lombok.core.AnnotationProcessor")); + //lombok: + { + //broken init: + FileObject processorClassesDir = + compileProcessor(""" + package lombok.core; + import java.util.Set; + import javax.annotation.processing.AbstractProcessor; + import javax.annotation.processing.ProcessingEnvironment; + import javax.annotation.processing.RoundEnvironment; + import javax.annotation.processing.SupportedAnnotationTypes; + import javax.lang.model.element.TypeElement; + + @SupportedAnnotationTypes("*") + public class AnnotationProcessor extends AbstractProcessor { + + @Override + public void init(ProcessingEnvironment processingEnv) { + throw new IllegalStateException("Broken."); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + throw new IllegalStateException("Should not get here."); + } + + } + """); + try { + processorPath.add(processorClassesDir); + FileObject test = FileUtil.createData(root1, "Test.java"); + TestUtilities.copyStringToFile(test, + """ + public class Test{} + """); + JavaSource source = JavaSource.forFileObject(test); + source.runUserActionTask(cc -> { + cc.toPhase(JavaSource.Phase.RESOLVED); + List messages = + cc.getDiagnostics() + .stream() + .map(d -> d.getMessage(null)) + .toList(); + if (messages.stream().noneMatch(msg -> msg.contains(Bundle.ERR_LombokException(NoJavacHelper.REQUIRED_JAVAC_VERSION, "Broken.")))) { + fail("Expected error not found, all messages: " + messages); + } + if (messages.stream().anyMatch(msg -> msg.contains("Should not get here"))) { + fail("Unexpected error found, all messages: " + messages); + } + }, true); + } finally { + processorPath.remove(processorClassesDir); + } + } + + { + //broken process: + FileObject processorClassesDir = + compileProcessor(""" + package lombok.core; + import java.util.Set; + import javax.annotation.processing.AbstractProcessor; + import javax.annotation.processing.ProcessingEnvironment; + import javax.annotation.processing.RoundEnvironment; + import javax.annotation.processing.SupportedAnnotationTypes; + import javax.lang.model.element.TypeElement; + + @SupportedAnnotationTypes("*") + public class AnnotationProcessor extends AbstractProcessor { + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + throw new IllegalStateException("Broken."); + } + + } + """); + try { + processorPath.add(processorClassesDir); + FileObject test = FileUtil.createData(root1, "Test.java"); + TestUtilities.copyStringToFile(test, + """ + public class Test{} + """); + JavaSource source = JavaSource.forFileObject(test); + source.runUserActionTask(cc -> { + cc.toPhase(JavaSource.Phase.RESOLVED); + List messages = + cc.getDiagnostics() + .stream() + .map(d -> d.getMessage(null)) + .toList(); + if (messages.stream().noneMatch(msg -> msg.contains(Bundle.ERR_LombokException(NoJavacHelper.REQUIRED_JAVAC_VERSION, "Broken.")))) { + fail("Expected error not found, all messages: " + messages); + } + if (messages.stream().anyMatch(msg -> msg.contains("Should not get here"))) { + fail("Unexpected error found, all messages: " + messages); + } + }, true); + } finally { + processorPath.remove(processorClassesDir); + } + } + } + + private FileObject compileProcessor(String processorCode) throws Exception { + FileObject wdFO = FileUtil.toFileObject(getWorkDir()); + FileObject processorSrcDir = FileUtil.createFolder(wdFO, "processor-src"); + FileObject processorClassesDir = FileUtil.createFolder(wdFO, "processor-classes"); + FileObject processorFO = FileUtil.createData(processorSrcDir, "AnnotationProcessor.java"); + TestUtilities.copyStringToFile(processorFO, + processorCode); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null)) { + boolean result = + compiler.getTask(null, + null, + null, + List.of("-d", FileUtil.toFile(processorClassesDir).getAbsolutePath(), + "--release", "21"), + null, + fm.getJavaFileObjects(FileUtil.toPath(processorFO))) + .call(); + assertTrue(result); + } + + return processorClassesDir; + } + private static final class MockHandler extends Handler { static FileObject currentRoot; @@ -324,10 +567,16 @@ public static final class MAP implements AnnotationProcessingQuery.Result { private final ChangeSupport listeners = new ChangeSupport(this); private volatile Set mode = Collections.emptySet(); + private volatile Set apsToRun = null; void setAnnotationProcessingEnabled(Set newMode) { + setAnnotationProcessingEnabled(newMode, null); + } + + void setAnnotationProcessingEnabled(Set newMode, Set newApsToRun) { this.mode = newMode; + this.apsToRun = newApsToRun; listeners.fireChange(); } @@ -338,7 +587,7 @@ public Set annotationProcessingEnabled() { @Override public Iterable annotationProcessorsToRun() { - return null; + return apsToRun; } @Override @@ -374,4 +623,7 @@ public AnnotationProcessingQuery.Result getAnnotationProcessingOptions(FileObjec } + static { + System.setProperty("SourcePath.no.source.filter", "true"); + } } From b3a448c82c980beb20cb34e24a8bfff4a3833722 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 13 Aug 2026 15:02:52 +0200 Subject: [PATCH 2/2] Trying to overcome Lombok's broken error recovery. --- .../org/netbeans/modules/java/source/indexing/APTUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/indexing/APTUtils.java b/java/java.source.base/src/org/netbeans/modules/java/source/indexing/APTUtils.java index 220c16a31019..624f2140422f 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/indexing/APTUtils.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/indexing/APTUtils.java @@ -964,7 +964,7 @@ private void save() { } } - private static final class ErrorToleratingProcessor implements Processor { + private final class ErrorToleratingProcessor implements Processor { private final Processor delegate; private ProcessingEnvironment processingEnv; @@ -1051,6 +1051,7 @@ private void reportError(ProcessingEnvironment processingEnv, Throwable t, Eleme String message; if (delegate.getClass().getName().startsWith(JavacParser.LOMBOK_ANNOTATION_PROCESSOR_PREFIX)) { message = Bundle.ERR_LombokException(NoJavacHelper.REQUIRED_JAVAC_VERSION, exception.toString()); + classLoaderCache = null; //Lombok may only crash with the exception out of `init` once per ClassLoader; clear the loader so that next time it fails again in the same way } else { message = Bundle.ERR_ProcessorException(delegate.getClass().getName(), exception.toString()); }