From b658d3e218230df1a457b41d09898fa2b0f89970 Mon Sep 17 00:00:00 2001 From: Alexander Kriegisch Date: Sun, 24 Dec 2023 10:04:52 +0700 Subject: [PATCH 1/6] Refactor javac output parsing code for better readability Add a few more helper methods, factor out error message constants into an inner class, get rid of some deprecated API usage, add or improve comments, remove excessive blank line usage. The scope of these changes is not the whole JavacCompiler class, but just the 'parseModern*' methods I am about to improve some more in subsequent commits. The functionality is unchanged for now, it really is a classical refactoring. --- .../plexus/compiler/javac/JavacCompiler.java | 286 ++++++++---------- 1 file changed, 134 insertions(+), 152 deletions(-) diff --git a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java index 0255c2a7..83412f69 100644 --- a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java +++ b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java @@ -62,6 +62,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Properties; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentLinkedDeque; @@ -80,10 +81,14 @@ import org.codehaus.plexus.util.cli.CommandLineUtils; import org.codehaus.plexus.util.cli.Commandline; +import static org.codehaus.plexus.compiler.CompilerMessage.Kind.*; +import static org.codehaus.plexus.compiler.javac.JavacCompiler.Messages.*; + /** * @author Trygve Laugstøl * @author Matthew Pocock * @author Jörg Waßmer + * @author Alexander Kriegisch * @author Others * */ @@ -91,21 +96,45 @@ @Singleton public class JavacCompiler extends AbstractCompiler { - // see compiler.warn.warning in compiler.properties of javac sources - private static final String[] WARNING_PREFIXES = {"warning: ", "\u8b66\u544a: ", "\u8b66\u544a\uff1a "}; + /** + * Multi-language compiler messages to parse from forked javac output. + * + * Instead of manually duplicating multi-language messages into this class, it would be preferable to fetch the + * strings directly from the running JDK: + *
{@code
+     * new JavacMessages("com.sun.tools.javac.resources.javac", Locale.getDefault())
+     *   .getLocalizedString("javac.msg.proc.annotation.uncaught.exception")
+     * }
+ * Hoewever, due to JMS module protection, it would be necessary to run Plexus Compiler (and hence also Maven + * Compiler and the whole Maven JVM) with {@code --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED} + * on more recent JDK versions. As this cannot be reliably expected and using internal APIs - even though stable + * since at least JDK 8 - it is not a future-proof approach. So we refrain from doing so, even though during Plexus + * Compiler development it might come in handy. + *

+ * TODO: Check compiler.properties and javac.properties in OpenJDK javac source code for + * message changes, relevant new messages, new locales. + */ + protected static class Messages { + // compiler.properties -> compiler.err.error (en, ja, zh_CN, de) + protected static final String[] ERROR_PREFIXES = {"error: ", "エラー: ", "错误: ", "Fehler: "}; + + // compiler.properties -> compiler.warn.warning (en, ja, zh_CN, de) + protected static final String[] WARNING_PREFIXES = {"warning: ", "警告: ", "警告: ", "Warnung: "}; - // see compiler.note.note in compiler.properties of javac sources - private static final String[] NOTE_PREFIXES = {"Note: ", "\u6ce8: ", "\u6ce8\u610f\uff1a "}; + // compiler.properties -> compiler.note.note (en, ja, zh_CN, de) + protected static final String[] NOTE_PREFIXES = {"Note: ", "ノート: ", "注: ", "Hinweis: "}; - // see compiler.misc.verbose in compiler.properties of javac sources - private static final String[] MISC_PREFIXES = {"["}; + // compiler.properties -> compiler.misc.verbose.* + protected static final String[] MISC_PREFIXES = {"["}; + } private static final Object LOCK = new Object(); - private static final String JAVAC_CLASSNAME = "com.sun.tools.javac.Main"; private volatile Class javacClass; - private final Deque> javacClasses = new ConcurrentLinkedDeque<>(); @Inject @@ -131,13 +160,11 @@ public String getCompilerId() { @Override public CompilerResult performCompile(CompilerConfiguration config) throws CompilerException { File destinationDir = new File(config.getOutputLocation()); - if (!destinationDir.exists()) { destinationDir.mkdirs(); } String[] sourceFiles = getSourceFiles(config); - if ((sourceFiles == null) || (sourceFiles.length == 0)) { return new CompilerResult(); } @@ -145,12 +172,10 @@ public CompilerResult performCompile(CompilerConfiguration config) throws Compil logCompiling(sourceFiles, config); String[] args = buildCompilerArguments(config, sourceFiles); - CompilerResult result; if (config.isFork()) { String executable = config.getExecutable(); - if (StringUtils.isEmpty(executable)) { try { executable = getJavacExecutable(); @@ -161,7 +186,6 @@ public CompilerResult performCompile(CompilerConfiguration config) throws Compil executable = "javac"; } } - result = compileOutOfProcess(config, executable, args); } else { if (isJava16() && !config.isForceJavacCompilerUse()) { @@ -200,9 +224,7 @@ public static String[] buildCompilerArguments(CompilerConfiguration config, Stri // ---------------------------------------------------------------------- File destinationDir = new File(config.getOutputLocation()); - args.add("-d"); - args.add(destinationDir.getAbsolutePath()); // ---------------------------------------------------------------------- @@ -212,14 +234,12 @@ public static String[] buildCompilerArguments(CompilerConfiguration config, Stri List classpathEntries = config.getClasspathEntries(); if (classpathEntries != null && !classpathEntries.isEmpty()) { args.add("-classpath"); - args.add(getPathString(classpathEntries)); } List modulepathEntries = config.getModulepathEntries(); if (modulepathEntries != null && !modulepathEntries.isEmpty()) { args.add("--module-path"); - args.add(getPathString(modulepathEntries)); } @@ -228,7 +248,6 @@ public static String[] buildCompilerArguments(CompilerConfiguration config, Stri // always pass source path, even if sourceFiles are declared, // needed for jsr269 annotation processing, see MCOMPILER-98 args.add("-sourcepath"); - args.add(getPathString(sourceLocations)); } if (!isJava16() || config.isForceJavacCompilerUse() || config.isFork()) { @@ -240,7 +259,6 @@ public static String[] buildCompilerArguments(CompilerConfiguration config, Stri if (config.getGeneratedSourcesDirectory() != null) { config.getGeneratedSourcesDirectory().mkdirs(); - args.add("-s"); args.add(config.getGeneratedSourcesDirectory().getAbsolutePath()); } @@ -255,7 +273,6 @@ public static String[] buildCompilerArguments(CompilerConfiguration config, Stri if (i > 0) { buffer.append(","); } - buffer.append(procs[i]); } args.add(buffer.toString()); @@ -366,13 +383,10 @@ public static String[] buildCompilerArguments(CompilerConfiguration config, Stri } args.add(key); - String value = entry.getValue(); - if (StringUtils.isEmpty(value)) { continue; } - args.add(value); } @@ -413,11 +427,9 @@ private static boolean isPreJava16(CompilerConfiguration config) { if (v == null) { v = config.getCompilerVersion(); } - if (v == null) { v = config.getSourceVersion(); } - if (v == null) { return true; } @@ -437,11 +449,9 @@ private static boolean isPreJava18(CompilerConfiguration config) { if (v == null) { v = config.getCompilerVersion(); } - if (v == null) { v = config.getSourceVersion(); } - if (v == null) { return true; } @@ -459,17 +469,14 @@ private static boolean isPreJava18(CompilerConfiguration config) { } private static boolean isPreJava9(CompilerConfiguration config) { - String v = config.getReleaseVersion(); if (v == null) { v = config.getCompilerVersion(); } - if (v == null) { v = config.getSourceVersion(); } - if (v == null) { return true; } @@ -510,7 +517,6 @@ protected CompilerResult compileOutOfProcess(CompilerConfiguration config, Strin Commandline cli = new Commandline(); cli.setWorkingDirectory(config.getWorkingDirectory().getAbsolutePath()); - cli.setExecutable(executable); try { @@ -522,7 +528,6 @@ protected CompilerResult compileOutOfProcess(CompilerConfiguration config, Strin if (!StringUtils.isEmpty(config.getMaxmem())) { cli.addArguments(new String[] {"-J-Xmx" + config.getMaxmem()}); } - if (!StringUtils.isEmpty(config.getMeminitial())) { cli.addArguments(new String[] {"-J-Xms" + config.getMeminitial()}); } @@ -537,9 +542,7 @@ protected CompilerResult compileOutOfProcess(CompilerConfiguration config, Strin } CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer(); - int returnCode; - List messages; if (getLog().isDebugEnabled()) { @@ -563,6 +566,11 @@ protected CompilerResult compileOutOfProcess(CompilerConfiguration config, Strin } try { + // TODO: + // Is it really helpful to parse stdOut and stdErr as a single stream, instead of taking the chance to + // draw extra information from the fact that normal javac output is written to stdOut, while warnings and + // errors are written to stdErr? Of course, chronological correlation of messages would be more difficult + // then, but basically, we are throwing away information here. returnCode = CommandLineUtils.executeCommandLine(cli, out, out); messages = parseModernStream(returnCode, new BufferedReader(new StringReader(out.getOutput()))); @@ -609,16 +617,12 @@ protected CompilerResult compileInProcessWithProperClassloader(Class javacCla */ private static CompilerResult compileInProcess0(Class javacClass, String[] args) throws CompilerException { StringWriter out = new StringWriter(); - Integer ok; - List messages; try { Method compile = javacClass.getMethod("compile", new Class[] {String[].class, PrintWriter.class}); - ok = (Integer) compile.invoke(null, new Object[] {args, new PrintWriter(out)}); - messages = parseModernStream(ok, new BufferedReader(new StringReader(out.toString()))); } catch (NoSuchMethodException | IOException | InvocationTargetException | IllegalAccessException e) { throw new CompilerException("Error while executing the compiler.", e); @@ -643,69 +647,21 @@ private static CompilerResult compileInProcess0(Class javacClass, String[] ar Pattern.compile("^(?:javac:|Error occurred during initialization of (?:boot layer|VM)).*", Pattern.DOTALL); /** - * Parse the output from the compiler into a list of CompilerMessage objects + * Parse the compiler output into a list of compiler messages * - * @param exitCode The exit code of javac. - * @param input The output of the compiler - * @return List of CompilerMessage objects - * @throws IOException + * @param exitCode javac exit code (0 on success, non-zero otherwise) + * @param input compiler output (stdOut and stdErr merged into input stream) + * @return list of {@link CompilerMessage} objects + * @throws IOException if there is a problem reading from the input reader */ static List parseModernStream(int exitCode, BufferedReader input) throws IOException { List errors = new ArrayList<>(); - String line; - StringBuilder buffer = new StringBuilder(); - boolean hasPointer = false; int stackTraceLineCount = 0; - while (true) { - line = input.readLine(); - - if (line == null) { - // javac output not detected by other parsing - // maybe better to ignore only the summary and mark the rest as error - String bufferAsString = buffer.toString(); - if (buffer.length() > 0) { - if (JAVAC_OR_JVM_ERROR.matcher(bufferAsString).matches()) { - errors.add(new CompilerMessage(bufferAsString, CompilerMessage.Kind.ERROR)); - } else if (hasPointer) { - // A compiler message remains in buffer at end of parse stream - errors.add(parseModernError(exitCode, bufferAsString)); - } else if (stackTraceLineCount > 0) { - // Extract stack trace from end of buffer - String[] lines = bufferAsString.split("\\R"); - int linesTotal = lines.length; - buffer = new StringBuilder(); - int firstLine = linesTotal - stackTraceLineCount; - - // Salvage Javac localized message 'javac.msg.bug' ("An exception has occurred in the - // compiler ... Please file a bug") - if (firstLine > 0) { - final String lineBeforeStackTrace = lines[firstLine - 1]; - // One of those two URL substrings should always appear, without regard to JVM locale. - // TODO: Update, if the URL changes, last checked for JDK 21. - if (lineBeforeStackTrace.contains("java.sun.com/webapps/bugreport") - || lineBeforeStackTrace.contains("bugreport.java.com")) { - firstLine--; - } - } - - // Note: For message 'javac.msg.proc.annotation.uncaught.exception' ("An annotation processor - // threw an uncaught exception"), there is no locale-independent substring, and the header is - // also multi-line. It was discarded in the removed method 'parseAnnotationProcessorStream', - // and we continue to do so. - - for (int i = firstLine; i < linesTotal; i++) { - buffer.append(lines[i]).append(EOL); - } - errors.add(new CompilerMessage(buffer.toString(), CompilerMessage.Kind.ERROR)); - } - } - return errors; - } - + while ((line = input.readLine()) != null) { if (stackTraceLineCount == 0 && STACK_TRACE_FIRST_LINE.matcher(line).matches() || STACK_TRACE_OTHER_LINE.matcher(line).matches()) { stackTraceLineCount++; @@ -717,33 +673,77 @@ static List parseModernStream(int exitCode, BufferedReader inpu if (!line.startsWith(" ") && hasPointer) { // add the error bean errors.add(parseModernError(exitCode, buffer.toString())); - // reset for next error block buffer = new StringBuilder(); // this is quicker than clearing it - hasPointer = false; } - // TODO: there should be a better way to parse these - if ((buffer.length() == 0) && line.startsWith("error: ")) { - errors.add(new CompilerMessage(line, CompilerMessage.Kind.ERROR)); - } else if ((buffer.length() == 0) && line.startsWith("warning: ")) { - errors.add(new CompilerMessage(line, CompilerMessage.Kind.WARNING)); - } else if ((buffer.length() == 0) && isNote(line)) { - // skip, JDK 1.5 telling us deprecated APIs are used but -Xlint:deprecation isn't set - } else if ((buffer.length() == 0) && isMisc(line)) { - // verbose output was set - errors.add(new CompilerMessage(line, CompilerMessage.Kind.OTHER)); + if (buffer.length() == 0) { + // try to classify output line by type (error, warning etc.) + // TODO: there should be a better way to parse these + if (isError(line)) { + errors.add(new CompilerMessage(line, ERROR)); + } else if (isWarning(line)) { + errors.add(new CompilerMessage(line, WARNING)); + } else if (isNote(line)) { + // skip, JDK telling us deprecated APIs are used but -Xlint:deprecation isn't set + } else if (isMisc(line)) { + // verbose output was set + errors.add(new CompilerMessage(line, CompilerMessage.Kind.OTHER)); + } else { + // add first unclassified line to buffer + buffer.append(line).append(EOL); + } } else { - buffer.append(line); - - buffer.append(EOL); + // add next unclassified line to buffer + buffer.append(line).append(EOL); } if (line.endsWith("^")) { hasPointer = true; } } + + // javac output not detected by other parsing + // maybe better to ignore only the summary and mark the rest as error + String bufferAsString = buffer.toString(); + if (!bufferAsString.isEmpty()) { + if (JAVAC_OR_JVM_ERROR.matcher(bufferAsString).matches()) { + errors.add(new CompilerMessage(bufferAsString, ERROR)); + } else if (hasPointer) { + // A compiler message remains in buffer at end of parse stream + errors.add(parseModernError(exitCode, bufferAsString)); + } else if (stackTraceLineCount > 0) { + // Extract stack trace from end of buffer + String[] lines = bufferAsString.split("\\R"); + int linesTotal = lines.length; + buffer = new StringBuilder(); + int firstLine = linesTotal - stackTraceLineCount; + + // Salvage Javac localized message 'javac.msg.bug' ("An exception has occurred in the + // compiler ... Please file a bug") + if (firstLine > 0) { + final String lineBeforeStackTrace = lines[firstLine - 1]; + // One of those two URL substrings should always appear, without regard to JVM locale. + // TODO: Update, if the URL changes, last checked for JDK 21. + if (lineBeforeStackTrace.contains("java.sun.com/webapps/bugreport") + || lineBeforeStackTrace.contains("bugreport.java.com")) { + firstLine--; + } + } + + // Note: For message 'javac.msg.proc.annotation.uncaught.exception' ("An annotation processor + // threw an uncaught exception"), there is no locale-independent substring, and the header is + // also multi-line. It was discarded in the removed method 'parseAnnotationProcessorStream', + // and we continue to do so. + + for (int i = firstLine; i < linesTotal; i++) { + buffer.append(lines[i]).append(EOL); + } + errors.add(new CompilerMessage(buffer.toString(), ERROR)); + } + } + return errors; } private static boolean isMisc(String line) { @@ -754,6 +754,14 @@ private static boolean isNote(String line) { return startsWithPrefix(line, NOTE_PREFIXES); } + private static boolean isWarning(String line) { + return startsWithPrefix(line, WARNING_PREFIXES); + } + + private static boolean isError(String line) { + return startsWithPrefix(line, ERROR_PREFIXES); + } + private static boolean startsWithPrefix(String line, String[] prefixes) { for (String prefix : prefixes) { if (line.startsWith(prefix)) { @@ -764,26 +772,23 @@ private static boolean startsWithPrefix(String line, String[] prefixes) { } /** - * Construct a CompilerMessage object from a line of the compiler output + * Construct a compiler message object from a compiler output line * - * @param exitCode The exit code from javac. - * @param error output line from the compiler - * @return the CompilerMessage object + * @param exitCode javac exit code + * @param error compiler output line + * @return compiler message object */ static CompilerMessage parseModernError(int exitCode, String error) { final StringTokenizer tokens = new StringTokenizer(error, ":"); - - boolean isError = exitCode != 0; + CompilerMessage.Kind messageKind = exitCode == 0 ? WARNING : ERROR; try { // With Java 6 error output lines from the compiler got longer. For backward compatibility - // .. and the time being, we eat up all (if any) tokens up to the erroneous file and source - // .. line indicator tokens. + // and the time being, we eat up all (if any) tokens up to the erroneous file and source + // line indicator tokens. boolean tokenIsAnInteger; - StringBuilder file = null; - String currentToken = null; do { @@ -796,9 +801,7 @@ static CompilerMessage parseModernError(int exitCode, String error) { } currentToken = tokens.nextToken(); - // Probably the only backward compatible means of checking if a string is an integer. - tokenIsAnInteger = true; try { @@ -809,50 +812,36 @@ static CompilerMessage parseModernError(int exitCode, String error) { } while (!tokenIsAnInteger); final String lineIndicator = currentToken; - - final int startOfFileName = file.toString().lastIndexOf(']'); - + final int startOfFileName = Objects.requireNonNull(file).toString().lastIndexOf(']'); if (startOfFileName > -1) { file = new StringBuilder(file.substring(startOfFileName + 1 + EOL.length())); } final int line = Integer.parseInt(lineIndicator); - final StringBuilder msgBuffer = new StringBuilder(); - String msg = tokens.nextToken(EOL).substring(2); // Remove the 'warning: ' prefix - final String warnPrefix = getWarnPrefix(msg); + final String warnPrefix = getWarningPrefix(msg); if (warnPrefix != null) { - isError = false; + messageKind = WARNING; msg = msg.substring(warnPrefix.length()); - } else { - isError = exitCode != 0; } - - msgBuffer.append(msg); - - msgBuffer.append(EOL); + msgBuffer.append(msg).append(EOL); String context = tokens.nextToken(EOL); - String pointer = null; do { final String msgLine = tokens.nextToken(EOL); - if (pointer != null) { msgBuffer.append(msgLine); - msgBuffer.append(EOL); } else if (msgLine.endsWith("^")) { pointer = msgLine; } else { msgBuffer.append(context); - msgBuffer.append(EOL); - context = msgLine; } } while (tokens.hasMoreTokens()); @@ -860,24 +849,22 @@ static CompilerMessage parseModernError(int exitCode, String error) { msgBuffer.append(EOL); final String message = msgBuffer.toString(); - - final int startcolumn = pointer.indexOf("^"); - + final int startcolumn = Objects.requireNonNull(pointer).indexOf("^"); int endcolumn = (context == null) ? startcolumn : context.indexOf(" ", startcolumn); - if (endcolumn == -1) { - endcolumn = context.length(); + endcolumn = Objects.requireNonNull(context).length(); } - return new CompilerMessage(file.toString(), isError, line, startcolumn, line, endcolumn, message.trim()); + return new CompilerMessage( + file.toString(), messageKind, line, startcolumn, line, endcolumn, message.trim()); } catch (NoSuchElementException e) { - return new CompilerMessage("no more tokens - could not parse error message: " + error, isError); + return new CompilerMessage("no more tokens - could not parse error message: " + error, messageKind); } catch (Exception e) { - return new CompilerMessage("could not parse error message: " + error, isError); + return new CompilerMessage("could not parse error message: " + error, messageKind); } } - private static String getWarnPrefix(String msg) { + private static String getWarningPrefix(String msg) { for (String warningPrefix : WARNING_PREFIXES) { if (msg.startsWith(warningPrefix)) { return warningPrefix; @@ -905,15 +892,11 @@ private File createFileWithArguments(String[] args, String outputDirectory) thro } writer = new PrintWriter(new FileWriter(tempFile)); - for (String arg : args) { String argValue = arg.replace(File.separatorChar, '/'); - writer.write("\"" + argValue + "\""); - writer.println(); } - writer.flush(); return tempFile; @@ -934,9 +917,9 @@ private File createFileWithArguments(String[] args, String outputDirectory) thro */ private static String getJavacExecutable() throws IOException { String javacCommand = "javac" + (Os.isFamily(Os.FAMILY_WINDOWS) ? ".exe" : ""); - String javaHome = System.getProperty("java.home"); File javacExe; + if (Os.isName("AIX")) { javacExe = new File(javaHome + File.separator + ".." + File.separator + "sh", javacCommand); } else if (Os.isName("Mac OS X")) { @@ -958,7 +941,6 @@ private static String getJavacExecutable() throws IOException { throw new IOException("The environment variable JAVA_HOME=" + javaHome + " doesn't exist or is not a valid directory."); } - javacExe = new File(env.getProperty("JAVA_HOME") + File.separator + "bin", javacCommand); } From 6c20fa80c00cbd9a4090cf544d87fcabb44ffdc6 Mon Sep 17 00:00:00 2001 From: Alexander Kriegisch Date: Tue, 26 Dec 2023 08:55:04 +0700 Subject: [PATCH 2/6] Improve forked javac error matching accuracy and flexibility - Add more error message prefixes to class JavacCompiler.Messages - New method JavacCompiler.getTextStartingWithPrefix handles multi-line Java properties with placeholders and match them correctly in javac log output - Add test verifying that for slightly modified, non-matching error headers at least the stack traces are still recognised and added as error messages, despite the headers missing in those cases --- .../plexus/compiler/javac/JavacCompiler.java | 195 +++++++++++++----- .../javac/ErrorMessageParserTest.java | 81 +++++--- .../compiler/javac/JavacCompilerTest.java | 30 ++- 3 files changed, 211 insertions(+), 95 deletions(-) diff --git a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java index 83412f69..f869126f 100644 --- a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java +++ b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java @@ -66,6 +66,7 @@ import java.util.Properties; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.codehaus.plexus.compiler.AbstractCompiler; @@ -129,6 +130,48 @@ protected static class Messages { // compiler.properties -> compiler.misc.verbose.* protected static final String[] MISC_PREFIXES = {"["}; + + // Generic javac error prefix + // TODO: In JDK 8, this generic prefix no longer seems to be in use for javac error messages, at least not in + // the Java part of javac. Maybe in C sources? Does javac even use any native classes? + protected static final String[] JAVAC_GENERIC_ERROR_PREFIXES = {"javac:"}; + + // Hard-coded, English-only error header in JVM native code, *not* followed by stack trace, but rather + // by another text message + protected static final String[] VM_INIT_ERROR_HEADERS = {"Error occurred during initialization of VM"}; + + // Hard-coded, English-only error header in class System, followed by stack trace + protected static final String[] BOOT_LAYER_INIT_ERROR_HEADERS = { + "Error occurred during initialization of boot layer" + }; + + // javac.properties-> javac.msg.proc.annotation.uncaught.exception + // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21) + protected static final String[] ANNOTATION_PROCESSING_ERROR_HEADERS = { + "\n\nAn annotation processor threw an uncaught exception.\nConsult the following stack trace for details.\n\n", + "\n\n注釈処理で捕捉されない例外がスローされました。\n詳細は次のスタック・トレースで調査してください。\n\n", + "\n\n批注处理程序抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n\n", + "\n\nAn annotation processor threw an uncaught exception.\nConsult the following stack trace for details.\n\n", + "\n\n注釈処理で捕捉されない例外がスローされました。\n詳細は次のスタックトレースで調査してください。\n\n", + "\n\n批注处理程序抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n\n", + "\n\nEin Annotationsprozessor hat eine nicht abgefangene Ausnahme ausgelöst.\nDetails finden Sie im folgenden Stacktrace.\n\n" + }; + + // javac.properties-> javac.msg.bug + // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-9, ja JDK-9, zh_CN JDK-9, en JDK-21, ja JDK-21, zh_CN JDK-21, de + // JDK-21) + protected static final String[] FILE_A_BUG_ERROR_HEADERS = { + "An exception has occurred in the compiler ({0}). Please file a bug at the Java Developer Connection (http://java.sun.com/webapps/bugreport) after checking the Bug Parade for duplicates. Include your program and the following diagnostic in your report. Thank you.\n", + "コンパイラで例外が発生しました({0})。Bug Paradeで重複がないかをご確認のうえ、Java Developer Connection (http://java.sun.com/webapps/bugreport)でbugの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を含めてください。ご協力ありがとうございます。\n", + "编译器 ({0}) 中出现异常错误。 如果在 Bug Parade 中没有找到该错误, 请在 Java Developer Connection (http://java.sun.com/webapps/bugreport) 中建立 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。\n", + "An exception has occurred in the compiler ({0}). Please file a bug against the Java compiler via the Java bug reporting page (http://bugreport.java.com) after checking the Bug Database (http://bugs.java.com) for duplicates. Include your program and the following diagnostic in your report. Thank you.", + "コンパイラで例外が発生しました({0})。Bug Database (http://bugs.java.com)で重複がないかをご確認のうえ、Java bugレポート・ページ(http://bugreport.java.com)でJavaコンパイラに対するbugの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を含めてください。ご協力ありがとうございます。", + "编译器 ({0}) 中出现异常错误。如果在 Bug Database (http://bugs.java.com) 中没有找到该错误, 请通过 Java Bug 报告页 (http://bugreport.java.com) 建立该 Java 编译器 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。", + "An exception has occurred in the compiler ({0}). Please file a bug against the Java compiler via the Java bug reporting page (https://bugreport.java.com) after checking the Bug Database (https://bugs.java.com) for duplicates. Include your program, the following diagnostic, and the parameters passed to the Java compiler in your report. Thank you.\n", + "コンパイラで例外が発生しました({0})。バグ・データベース(https://bugs.java.com)で重複がないかをご確認のうえ、Javaのバグ・レポート・ページ(https://bugreport.java.com)から、Javaコンパイラに対するバグの登録をお願いいたします。レポートには、該当のプログラム、次の診断内容、およびJavaコンパイラに渡されたパラメータをご入力ください。ご協力ありがとうございます。\n", + "编译器 ({0}) 中出现异常错误。如果在 Bug Database (https://bugs.java.com) 中没有找到有关该错误的 Java 编译器 Bug,请通过 Java Bug 报告页 (https://bugreport.java.com) 提交 Java 编译器 Bug。请在报告中附上您的程序、以下诊断信息以及传递到 Java 编译器的参数。谢谢。\n", + "Im Compiler ({0}) ist eine Ausnahme aufgetreten. Erstellen Sie auf der Java-Seite zum Melden von Bugs (https://bugreport.java.com) einen Bugbericht, nachdem Sie die Bugdatenbank (https://bugs.java.com) auf Duplikate geprüft haben. Geben Sie in Ihrem Bericht Ihr Programm, die folgende Diagnose und die Parameter an, die Sie dem Java-Compiler übergeben haben. Vielen Dank.\n" + }; } private static final Object LOCK = new Object(); @@ -642,10 +685,6 @@ private static CompilerResult compileInProcess0(Class javacClass, String[] ar private static final Pattern STACK_TRACE_OTHER_LINE = Pattern.compile("^(?:Caused by:\\s.*|\\s*at .*|\\s*\\.\\.\\.\\s\\d+\\smore)$"); - // Match generic javac errors with 'javac:' prefix, JMV init and boot layer init errors - private static final Pattern JAVAC_OR_JVM_ERROR = - Pattern.compile("^(?:javac:|Error occurred during initialization of (?:boot layer|VM)).*", Pattern.DOTALL); - /** * Parse the compiler output into a list of compiler messages * @@ -704,73 +743,131 @@ static List parseModernStream(int exitCode, BufferedReader inpu } } + String bufferContent = buffer.toString(); + if (bufferContent.isEmpty()) { + return errors; + } + // javac output not detected by other parsing // maybe better to ignore only the summary and mark the rest as error - String bufferAsString = buffer.toString(); - if (!bufferAsString.isEmpty()) { - if (JAVAC_OR_JVM_ERROR.matcher(bufferAsString).matches()) { - errors.add(new CompilerMessage(bufferAsString, ERROR)); - } else if (hasPointer) { - // A compiler message remains in buffer at end of parse stream - errors.add(parseModernError(exitCode, bufferAsString)); - } else if (stackTraceLineCount > 0) { - // Extract stack trace from end of buffer - String[] lines = bufferAsString.split("\\R"); - int linesTotal = lines.length; - buffer = new StringBuilder(); - int firstLine = linesTotal - stackTraceLineCount; - - // Salvage Javac localized message 'javac.msg.bug' ("An exception has occurred in the - // compiler ... Please file a bug") - if (firstLine > 0) { - final String lineBeforeStackTrace = lines[firstLine - 1]; - // One of those two URL substrings should always appear, without regard to JVM locale. - // TODO: Update, if the URL changes, last checked for JDK 21. - if (lineBeforeStackTrace.contains("java.sun.com/webapps/bugreport") - || lineBeforeStackTrace.contains("bugreport.java.com")) { - firstLine--; - } - } - - // Note: For message 'javac.msg.proc.annotation.uncaught.exception' ("An annotation processor - // threw an uncaught exception"), there is no locale-independent substring, and the header is - // also multi-line. It was discarded in the removed method 'parseAnnotationProcessorStream', - // and we continue to do so. - - for (int i = firstLine; i < linesTotal; i++) { - buffer.append(lines[i]).append(EOL); - } - errors.add(new CompilerMessage(buffer.toString(), ERROR)); + String cleanedUpMessage; + if ((cleanedUpMessage = getJavacGenericError(bufferContent)) != null + || (cleanedUpMessage = getBootLayerInitError(bufferContent)) != null + || (cleanedUpMessage = getVMInitError(bufferContent)) != null + || (cleanedUpMessage = getFileABugError(bufferContent)) != null + || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null) { + errors.add(new CompilerMessage(cleanedUpMessage, ERROR)); + } else if (hasPointer) { + // A compiler message remains in buffer at end of parse stream + errors.add(parseModernError(exitCode, bufferContent)); + } else if (stackTraceLineCount > 0) { + // Extract stack trace from end of buffer + String[] lines = bufferContent.split("\\R"); + int linesTotal = lines.length; + buffer = new StringBuilder(); + int firstLine = linesTotal - stackTraceLineCount; + for (int i = firstLine; i < linesTotal; i++) { + buffer.append(lines[i]).append(EOL); } + errors.add(new CompilerMessage(buffer.toString(), ERROR)); } + // TODO: Add something like this? Check if it creates more value or more unnecessary log output in general. + // else { + // // Fall-back, if still no error or stack trace was recognised + // errors.add(new CompilerMessage(bufferContent, exitCode == 0 ? OTHER : ERROR)); + // } + return errors; } - private static boolean isMisc(String line) { - return startsWithPrefix(line, MISC_PREFIXES); + private static boolean isMisc(String message) { + return startsWithPrefix(message, MISC_PREFIXES); + } + + private static boolean isNote(String message) { + return startsWithPrefix(message, NOTE_PREFIXES); + } + + private static boolean isWarning(String message) { + return startsWithPrefix(message, WARNING_PREFIXES); + } + + private static boolean isError(String message) { + return startsWithPrefix(message, ERROR_PREFIXES); } - private static boolean isNote(String line) { - return startsWithPrefix(line, NOTE_PREFIXES); + private static String getJavacGenericError(String message) { + return getTextStartingWithPrefix(message, JAVAC_GENERIC_ERROR_PREFIXES); } - private static boolean isWarning(String line) { - return startsWithPrefix(line, WARNING_PREFIXES); + private static String getVMInitError(String message) { + return getTextStartingWithPrefix(message, VM_INIT_ERROR_HEADERS); } - private static boolean isError(String line) { - return startsWithPrefix(line, ERROR_PREFIXES); + private static String getBootLayerInitError(String message) { + return getTextStartingWithPrefix(message, BOOT_LAYER_INIT_ERROR_HEADERS); } - private static boolean startsWithPrefix(String line, String[] prefixes) { + private static String getFileABugError(String message) { + return getTextStartingWithPrefix(message, FILE_A_BUG_ERROR_HEADERS); + } + + private static String getAnnotationProcessingError(String message) { + return getTextStartingWithPrefix(message, ANNOTATION_PROCESSING_ERROR_HEADERS); + } + + private static boolean startsWithPrefix(String text, String[] prefixes) { for (String prefix : prefixes) { - if (line.startsWith(prefix)) { + if (text.startsWith(prefix)) { return true; } } return false; } + /** + * Identify and return a known javac error message prefix and all subsequent text - usually a stack trace - from a + * javac log output buffer. + * + * @param text log buffer to search for a javac error message stack trace + * @param prefixes array of strings in Java properties format, e.g. {@code "some error with line feed\nand parameter + * placeholders {0} and {1}"} in multiple locales (hence the array). For the search, the + * placeholders may be represented by any text in the log buffer. + * @return if found, the error message + all subsequent text, otherwise {@code null} + */ + static String getTextStartingWithPrefix(String text, String[] prefixes) { + // Implementation note: The properties format with placeholders makes it easy to just copy & paste values from + // the JDK compared to having to convert them to regular expressions with ".*" instead of "{0}" and quote + // special regex characters. This makes the implementation of this method more complex and potentially a bit + // slower, but hopefully is worth the effort for the convenience of future developers maintaining this class. + + // Normalise line feeds to the UNIX format found in JDK multi-line messages in properties files + text = text.replaceAll("\\R", "\n"); + + // Search text for given error message prefixes/headers, until the first match is found + for (String prefix : prefixes) { + // Split properties message along placeholders like "{0}", "{1}" etc. + String[] prefixParts = prefix.split("\\{\\d+\\}"); + for (int i = 0; i < prefixParts.length; i++) { + // Make sure to treat split sections as literal text in search regex by enclosing them in "\Q" and "\E". + // See https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html, search for "Quotation". + prefixParts[i] = "\\Q" + prefixParts[i] + "\\E"; + } + // Join message parts, replacing properties placeholders by ".*" regex ones + prefix = String.join(".*?", prefixParts); + // Find prefix + subsequent text in Pattern.DOTALL mode, represented in regex as "(?s)". + // This matches across line break boundaries. + Matcher matcher = Pattern.compile("(?s).*(" + prefix + ".*)").matcher(text); + if (matcher.matches()) { + // Match -> cut off text before header and replace UNIX line breaks by platform ones again + return matcher.replaceFirst("$1").replaceAll("\n", EOL); + } + } + + // No match + return null; + } + /** * Construct a compiler message object from a compiler output line * diff --git a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java index 5ab3c822..a91d300b 100644 --- a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java +++ b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java @@ -37,8 +37,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.codehaus.plexus.compiler.javac.JavacCompiler.Messages.*; +import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; @@ -50,6 +50,8 @@ */ public class ErrorMessageParserTest { private static final String EOL = System.getProperty("line.separator"); + private static final String UNIDENTIFIED_LOG_LINES = + "These log lines should be cut off\n" + "when preceding known error message headers\n"; @Test public void testDeprecationMessage() throws Exception { @@ -780,10 +782,39 @@ public void testJava7Error() throws Exception { assertThat(message2.getEndLine(), is(3)); } + @ParameterizedTest(name = "{0}") + @MethodSource("testStackTraceWithUnknownHeader_args") + public void testStackTraceWithUnknownHeader(String scenario, String stackTraceHeader) throws Exception { + String stackTraceWithHeader = UNIDENTIFIED_LOG_LINES + stackTraceHeader + stackTraceInternalCompilerError; + + List compilerMessages = + JavacCompiler.parseModernStream(4, new BufferedReader(new StringReader(stackTraceWithHeader))); + + assertThat(compilerMessages, notNullValue()); + assertThat(compilerMessages, hasSize(1)); + + String message = compilerMessages.get(0).getMessage().replaceAll(EOL, "\n"); + // Parser retains neither unidentified log lines nor slightly modified stack trace header + assertThat(message, not(containsString(UNIDENTIFIED_LOG_LINES))); + assertThat(message, not(containsString(stackTraceHeader))); + // Parser returns stack strace without any preceding lines + assertThat(message, startsWith(stackTraceInternalCompilerError)); + } + + private static Stream testStackTraceWithUnknownHeader_args() { + return Stream.of( + Arguments.of( + "modified compiler error header", + FILE_A_BUG_ERROR_HEADERS[0].replaceAll("\\{0\\}", "21").replaceAll("bug", "beetle")), + Arguments.of( + "modified annotation processor error header", + ANNOTATION_PROCESSING_ERROR_HEADERS[0].replaceAll("uncaught", "undandled"))); + } + @ParameterizedTest(name = "{0}") @MethodSource("testBugParade_args") public void testBugParade(String jdkAndLocale, String stackTraceHeader) throws Exception { - String stackTraceWithHeader = stackTraceHeader + stackTraceInternalCompilerError; + String stackTraceWithHeader = UNIDENTIFIED_LOG_LINES + stackTraceHeader + stackTraceInternalCompilerError; List compilerMessages = JavacCompiler.parseModernStream(4, new BufferedReader(new StringReader(stackTraceWithHeader))); @@ -798,7 +829,8 @@ public void testBugParade(String jdkAndLocale, String stackTraceHeader) throws E } private static final String stackTraceInternalCompilerError = - "\tat com.sun.tools.javac.comp.MemberEnter.baseEnv(MemberEnter.java:1388)\n" + "com.sun.tools.javac.code.Symbol$CompletionFailure: class file for java.util.Optional not found\n" + + "\tat com.sun.tools.javac.comp.MemberEnter.baseEnv(MemberEnter.java:1388)\n" + "\tat com.sun.tools.javac.comp.MemberEnter.complete(MemberEnter.java:1046)\n" + "\tat com.sun.tools.javac.code.Symbol.complete(Symbol.java:574)\n" + "\tat com.sun.tools.javac.code.Symbol$ClassSymbol.complete(Symbol.java:1037)\n" @@ -832,27 +864,16 @@ public void testBugParade(String jdkAndLocale, String stackTraceHeader) throws E private static Stream testBugParade_args() { return Stream.of( - Arguments.of( - "JDK 8 English", - "An exception has occurred in the compiler ({0}). Please file a bug at the Java Developer Connection (http://java.sun.com/webapps/bugreport) after checking the Bug Parade for duplicates. Include your program and the following diagnostic in your report. Thank you.\n"), - Arguments.of( - "JDK 8 Japanese", - "コンパイラで例外が発生しました({0})。Bug Paradeで重複がないかをご確認のうえ、Java Developer Connection (http://java.sun.com/webapps/bugreport)でbugの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を含めてください。ご協力ありがとうございます。\n"), - Arguments.of( - "JDK 8 Chinese", - "编译器 ({0}) 中出现异常错误。 如果在 Bug Parade 中没有找到该错误, 请在 Java Developer Connection (http://java.sun.com/webapps/bugreport) 中建立 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。\n"), - Arguments.of( - "JDK 21 English", - "An exception has occurred in the compiler ({0}). Please file a bug against the Java compiler via the Java bug reporting page (https://bugreport.java.com) after checking the Bug Database (https://bugs.java.com) for duplicates. Include your program, the following diagnostic, and the parameters passed to the Java compiler in your report. Thank you.\n"), - Arguments.of( - "JDK 21 Japanese", - "コンパイラで例外が発生しました({0})。バグ・データベース(https://bugs.java.com)で重複がないかをご確認のうえ、Javaのバグ・レポート・ページ(https://bugreport.java.com)から、Javaコンパイラに対するバグの登録をお願いいたします。レポートには、該当のプログラム、次の診断内容、およびJavaコンパイラに渡されたパラメータをご入力ください。ご協力ありがとうございます。\n"), - Arguments.of( - "JDK 21 Chinese", - "编译器 ({0}) 中出现异常错误。如果在 Bug Database (https://bugs.java.com) 中没有找到有关该错误的 Java 编译器 Bug,请通过 Java Bug 报告页 (https://bugreport.java.com) 提交 Java 编译器 Bug。请在报告中附上您的程序、以下诊断信息以及传递到 Java 编译器的参数。谢谢。\n"), - Arguments.of( - "JDK 21 German", - "Im Compiler ({0}) ist eine Ausnahme aufgetreten. Erstellen Sie auf der Java-Seite zum Melden von Bugs (https://bugreport.java.com) einen Bugbericht, nachdem Sie die Bugdatenbank (https://bugs.java.com) auf Duplikate geprüft haben. Geben Sie in Ihrem Bericht Ihr Programm, die folgende Diagnose und die Parameter an, die Sie dem Java-Compiler übergeben haben. Vielen Dank.\n")); + Arguments.of("JDK 8 English", FILE_A_BUG_ERROR_HEADERS[0].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 8 Japanese", FILE_A_BUG_ERROR_HEADERS[1].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 8 Chinese", FILE_A_BUG_ERROR_HEADERS[2].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 9 English", FILE_A_BUG_ERROR_HEADERS[3].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 9 Japanese", FILE_A_BUG_ERROR_HEADERS[4].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 9 Chinese", FILE_A_BUG_ERROR_HEADERS[5].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 21 English", FILE_A_BUG_ERROR_HEADERS[6].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 21 Japanese", FILE_A_BUG_ERROR_HEADERS[7].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 21 Chinese", FILE_A_BUG_ERROR_HEADERS[8].replaceFirst("\\{0\\}", "21")), + Arguments.of("JDK 21 German", FILE_A_BUG_ERROR_HEADERS[9].replaceFirst("\\{0\\}", "21"))); } @Test @@ -1011,28 +1032,30 @@ public void testIssue37() throws IOException { @Test public void testJvmBootLayerInitializationError() throws Exception { - String out = "Error occurred during initialization of boot layer" + EOL + String out = "Error occurred during initialization of boot layer\n" + "java.lang.module.FindException: Module java.xml.bind not found"; List compilerErrors = - JavacCompiler.parseModernStream(1, new BufferedReader(new StringReader(out))); + JavacCompiler.parseModernStream(1, new BufferedReader(new StringReader(UNIDENTIFIED_LOG_LINES + out))); assertThat(compilerErrors, notNullValue()); assertThat(compilerErrors.size(), is(1)); assertThat(compilerErrors.get(0).getKind(), is(CompilerMessage.Kind.ERROR)); + assertThat(compilerErrors.get(0).getMessage().replaceAll(EOL, "\n"), startsWith(out)); } @Test public void testJvmInitializationError() throws Exception { - String out = "Error occurred during initialization of VM" + EOL + String out = "Error occurred during initialization of VM\n" + "Initial heap size set to a larger value than the maximum heap size"; List compilerErrors = - JavacCompiler.parseModernStream(1, new BufferedReader(new StringReader(out))); + JavacCompiler.parseModernStream(1, new BufferedReader(new StringReader(UNIDENTIFIED_LOG_LINES + out))); assertThat(compilerErrors, notNullValue()); assertThat(compilerErrors.size(), is(1)); assertThat(compilerErrors.get(0).getKind(), is(CompilerMessage.Kind.ERROR)); + assertThat(compilerErrors.get(0).getMessage().replaceAll(EOL, "\n"), startsWith(out)); } @Test diff --git a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/JavacCompilerTest.java b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/JavacCompilerTest.java index 8a182fcf..175905cd 100644 --- a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/JavacCompilerTest.java +++ b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/JavacCompilerTest.java @@ -12,8 +12,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.codehaus.plexus.compiler.javac.JavacCompiler.Messages.*; import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; @@ -44,6 +44,8 @@ */ public class JavacCompilerTest extends AbstractJavacCompilerTest { private static final String EOL = System.getProperty("line.separator"); + private static final String UNIDENTIFIABLE_LOG_LINES = + "These log lines should be cut off\n" + "when preceding known error message headers\n"; @BeforeEach public void setUp() { @@ -55,7 +57,7 @@ public void setUp() { @MethodSource("testParseModernStream_withAnnotationProcessingErrors_args") void testParseModernStream_withAnnotationProcessingErrors(String jdkAndLocale, String stackTraceHeader) throws IOException { - String stackTraceWithHeader = stackTraceHeader + stackTraceAnnotationProcessingError; + String stackTraceWithHeader = UNIDENTIFIABLE_LOG_LINES + stackTraceHeader + stackTraceAnnotationProcessingError; List compilerMessages = JavacCompiler.parseModernStream(1, new BufferedReader(new StringReader(stackTraceWithHeader))); @@ -63,8 +65,8 @@ void testParseModernStream_withAnnotationProcessingErrors(String jdkAndLocale, S assertThat(compilerMessages, hasSize(1)); String message = compilerMessages.get(0).getMessage().replaceAll(EOL, "\n"); - // Parser does not retain stack trace header, because it is hard to identify in a locale-independent way - assertThat(message, not(startsWith(stackTraceHeader))); + // Parser retains stack trace header + assertThat(message, startsWith(stackTraceHeader)); assertThat(message, endsWith(stackTraceAnnotationProcessingError)); } @@ -89,18 +91,12 @@ void testParseModernStream_withAnnotationProcessingErrors(String jdkAndLocale, S private static Stream testParseModernStream_withAnnotationProcessingErrors_args() { return Stream.of( - Arguments.of( - "JDK 8 English", - "\n\nAn annotation processor threw an uncaught exception.\nConsult the following stack trace for details.\n\n"), - Arguments.of("JDK 8 Japanese", "\n\n注釈処理で捕捉されない例外がスローされました。\n詳細は次のスタック・トレースで調査してください。\n\n"), - Arguments.of("JDK 8 Chinese", "\n\n注释处理程序抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n\n"), - Arguments.of( - "JDK 21 English", - "\n\nAn annotation processor threw an uncaught exception.\nConsult the following stack trace for details.\n\n"), - Arguments.of("JDK 21 Japanese", "\n\n注釈処理で捕捉されない例外がスローされました。\n詳細は次のスタックトレースで調査してください。\n\n"), - Arguments.of("JDK 21 Chinese", "\n\n批注处理程序抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n\n"), - Arguments.of( - "JDK 21 German", - "\n\nEin Annotationsprozessor hat eine nicht abgefangene Ausnahme ausgelöst.\nDetails finden Sie im folgenden Stacktrace.\n\n")); + Arguments.of("JDK 8 English", ANNOTATION_PROCESSING_ERROR_HEADERS[0]), + Arguments.of("JDK 8 Japanese", ANNOTATION_PROCESSING_ERROR_HEADERS[1]), + Arguments.of("JDK 8 Chinese", ANNOTATION_PROCESSING_ERROR_HEADERS[2]), + Arguments.of("JDK 21 English", ANNOTATION_PROCESSING_ERROR_HEADERS[3]), + Arguments.of("JDK 21 Japanese", ANNOTATION_PROCESSING_ERROR_HEADERS[4]), + Arguments.of("JDK 21 Chinese", ANNOTATION_PROCESSING_ERROR_HEADERS[5]), + Arguments.of("JDK 21 German", ANNOTATION_PROCESSING_ERROR_HEADERS[6])); } } From b7a6a2a573ee483ff056bf07bf242e27a2133405 Mon Sep 17 00:00:00 2001 From: Alexander Kriegisch Date: Tue, 26 Dec 2023 12:20:41 +0700 Subject: [PATCH 3/6] Recognise javac.msg.resource error header "The system is out of resources. Consult the following stack trace for details." --- .../plexus/compiler/javac/JavacCompiler.java | 19 ++++++- .../javac/ErrorMessageParserTest.java | 55 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java index f869126f..9fdd5240 100644 --- a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java +++ b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java @@ -172,6 +172,18 @@ protected static class Messages { "编译器 ({0}) 中出现异常错误。如果在 Bug Database (https://bugs.java.com) 中没有找到有关该错误的 Java 编译器 Bug,请通过 Java Bug 报告页 (https://bugreport.java.com) 提交 Java 编译器 Bug。请在报告中附上您的程序、以下诊断信息以及传递到 Java 编译器的参数。谢谢。\n", "Im Compiler ({0}) ist eine Ausnahme aufgetreten. Erstellen Sie auf der Java-Seite zum Melden von Bugs (https://bugreport.java.com) einen Bugbericht, nachdem Sie die Bugdatenbank (https://bugs.java.com) auf Duplikate geprüft haben. Geben Sie in Ihrem Bericht Ihr Programm, die folgende Diagnose und die Parameter an, die Sie dem Java-Compiler übergeben haben. Vielen Dank.\n" }; + + // javac.properties-> javac.msg.resource + // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21) + protected static final String[] SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS = { + "\n\nThe system is out of resources.\nConsult the following stack trace for details.\n", + "\n\nシステム・リソースが不足しています。\n詳細は次のスタック・トレースで調査してください。\n", + "\n\n系统资源不足。\n有关详细信息, 请参阅以下堆栈跟踪。\n", + "\n\nThe system is out of resources.\nConsult the following stack trace for details.\n", + "\n\nシステム・リソースが不足しています。\n詳細は次のスタックトレースで調査してください。\n", + "\n\n系统资源不足。\n有关详细信息, 请参阅以下堆栈跟踪。\n", + "\n\nDas System hat keine Ressourcen mehr.\nDetails finden Sie im folgenden Stacktrace.\n" + }; } private static final Object LOCK = new Object(); @@ -755,7 +767,8 @@ static List parseModernStream(int exitCode, BufferedReader inpu || (cleanedUpMessage = getBootLayerInitError(bufferContent)) != null || (cleanedUpMessage = getVMInitError(bufferContent)) != null || (cleanedUpMessage = getFileABugError(bufferContent)) != null - || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null) { + || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null + || (cleanedUpMessage = getSystemOutOfResourcesError(bufferContent)) != null) { errors.add(new CompilerMessage(cleanedUpMessage, ERROR)); } else if (hasPointer) { // A compiler message remains in buffer at end of parse stream @@ -816,6 +829,10 @@ private static String getAnnotationProcessingError(String message) { return getTextStartingWithPrefix(message, ANNOTATION_PROCESSING_ERROR_HEADERS); } + private static String getSystemOutOfResourcesError(String message) { + return getTextStartingWithPrefix(message, SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS); + } + private static boolean startsWithPrefix(String text, String[] prefixes) { for (String prefix : prefixes) { if (text.startsWith(prefix)) { diff --git a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java index a91d300b..c4392473 100644 --- a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java +++ b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java @@ -808,7 +808,10 @@ private static Stream testStackTraceWithUnknownHeader_args() { FILE_A_BUG_ERROR_HEADERS[0].replaceAll("\\{0\\}", "21").replaceAll("bug", "beetle")), Arguments.of( "modified annotation processor error header", - ANNOTATION_PROCESSING_ERROR_HEADERS[0].replaceAll("uncaught", "undandled"))); + ANNOTATION_PROCESSING_ERROR_HEADERS[0].replaceAll("uncaught", "undandled")), + Arguments.of( + "modified out of resources error header", + SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[0].replaceAll("resources", "memory"))); } @ParameterizedTest(name = "{0}") @@ -876,6 +879,56 @@ private static Stream testBugParade_args() { Arguments.of("JDK 21 German", FILE_A_BUG_ERROR_HEADERS[9].replaceFirst("\\{0\\}", "21"))); } + @ParameterizedTest(name = "{0}") + @MethodSource("testSystemOutOfResourcesError_args") + public void testSystemOutOfResourcesError(String jdkAndLocale, String stackTraceHeader) throws Exception { + String stackTraceWithHeader = UNIDENTIFIED_LOG_LINES + stackTraceHeader + stackTraceSystemOutOfResourcesError; + + List compilerMessages = + JavacCompiler.parseModernStream(4, new BufferedReader(new StringReader(stackTraceWithHeader))); + + assertThat(compilerMessages, notNullValue()); + assertThat(compilerMessages, hasSize(1)); + + String message = compilerMessages.get(0).getMessage().replaceAll(EOL, "\n"); + // Parser retains stack trace header + assertThat(message, startsWith(stackTraceHeader)); + assertThat(message, endsWith(stackTraceSystemOutOfResourcesError)); + } + + private static final String stackTraceSystemOutOfResourcesError = + "java.lang.OutOfMemoryError: GC overhead limit exceeded\n" + + "\tat com.sun.tools.javac.util.List.of(List.java:135)\n" + + "\tat com.sun.tools.javac.util.ListBuffer.append(ListBuffer.java:129)\n" + + "\tat com.sun.tools.javac.parser.JavacParser.variableDeclaratorsRest(JavacParser.java:3006)\n" + + "\tat com.sun.tools.javac.parser.JavacParser.classOrInterfaceBodyDeclaration(JavacParser.java:3537)\n" + + "\tat com.sun.tools.javac.parser.JavacParser.classOrInterfaceBody(JavacParser.java:3436)\n" + + "\tat com.sun.tools.javac.parser.JavacParser.classDeclaration(JavacParser.java:3285)\n" + + "\tat com.sun.tools.javac.parser.JavacParser.classOrInterfaceOrEnumDeclaration(JavacParser.java:3226)\n" + + "\tat com.sun.tools.javac.parser.JavacParser.typeDeclaration(JavacParser.java:3215)\n" + + "\tat com.sun.tools.javac.parser.JavacParser.parseCompilationUnit(JavacParser.java:3155)\n" + + "\tat com.sun.tools.javac.main.JavaCompiler.parse(JavaCompiler.java:628)\n" + + "\tat com.sun.tools.javac.main.JavaCompiler.parse(JavaCompiler.java:665)\n" + + "\tat com.sun.tools.javac.main.JavaCompiler.parseFiles(JavaCompiler.java:950)\n" + + "\tat com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:857)\n" + + "\tat com.sun.tools.javac.main.Main.compile(Main.java:523)\n" + + "\tat com.sun.tools.javac.main.Main.compile(Main.java:381)\n" + + "\tat com.sun.tools.javac.main.Main.compile(Main.java:370)\n" + + "\tat com.sun.tools.javac.main.Main.compile(Main.java:361)\n" + + "\tat com.sun.tools.javac.Main.compile(Main.java:56)\n" + + "\tat com.sun.tools.javac.Main.main(Main.java:42)\n"; + + private static Stream testSystemOutOfResourcesError_args() { + return Stream.of( + Arguments.of("JDK 8 English", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[0]), + Arguments.of("JDK 8 Japanese", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[1]), + Arguments.of("JDK 8 Chinese", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[2]), + Arguments.of("JDK 21 English", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[3]), + Arguments.of("JDK 21 Japanese", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[4]), + Arguments.of("JDK 21 Chinese", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[5]), + Arguments.of("JDK 21 German", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[6])); + } + @Test public void testNonAnchoredWarning() throws IOException { final String error = "warning: [options] bootstrap class path not set in conjunction with -source 1.6" + EOL From aeba4dbcb5eaaa5af810481be9115c4098dcafff Mon Sep 17 00:00:00 2001 From: Alexander Kriegisch Date: Tue, 26 Dec 2023 10:38:31 +0700 Subject: [PATCH 4/6] Recognise javac.msg.io error header "An input/output error occurred. Consult the following stack trace for details." --- .../plexus/compiler/javac/JavacCompiler.java | 20 +++++++- .../javac/ErrorMessageParserTest.java | 50 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java index 9fdd5240..823ea36a 100644 --- a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java +++ b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java @@ -184,6 +184,19 @@ protected static class Messages { "\n\n系统资源不足。\n有关详细信息, 请参阅以下堆栈跟踪。\n", "\n\nDas System hat keine Ressourcen mehr.\nDetails finden Sie im folgenden Stacktrace.\n" }; + + // javac.properties-> javac.msg.io + // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21) + protected static final String[] IO_ERROR_HEADERS = { + "\n\nAn input/output error occurred.\nConsult the following stack trace for details.\n", + "\n\n入出力エラーが発生しました。\n詳細は次のスタック・トレースで調査してください。\n", + "\n\n发生输入/输出错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n", + "\n\nAn input/output error occurred.\nConsult the following stack trace for details.\n", + "\n\n入出力エラーが発生しました。\n詳細は次のスタックトレースで調査してください。\n", + "\n\n发生输入/输出错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n", + "\n\nEin Eingabe-/Ausgabefehler ist aufgetreten.\nDetails finden Sie im folgenden Stacktrace.\n" + }; + } private static final Object LOCK = new Object(); @@ -768,7 +781,8 @@ static List parseModernStream(int exitCode, BufferedReader inpu || (cleanedUpMessage = getVMInitError(bufferContent)) != null || (cleanedUpMessage = getFileABugError(bufferContent)) != null || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null - || (cleanedUpMessage = getSystemOutOfResourcesError(bufferContent)) != null) { + || (cleanedUpMessage = getSystemOutOfResourcesError(bufferContent)) != null + || (cleanedUpMessage = getIOError(bufferContent)) != null) { errors.add(new CompilerMessage(cleanedUpMessage, ERROR)); } else if (hasPointer) { // A compiler message remains in buffer at end of parse stream @@ -833,6 +847,10 @@ private static String getSystemOutOfResourcesError(String message) { return getTextStartingWithPrefix(message, SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS); } + private static String getIOError(String message) { + return getTextStartingWithPrefix(message, IO_ERROR_HEADERS); + } + private static boolean startsWithPrefix(String text, String[] prefixes) { for (String prefix : prefixes) { if (text.startsWith(prefix)) { diff --git a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java index c4392473..1b7ca20b 100644 --- a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java +++ b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java @@ -811,7 +811,8 @@ private static Stream testStackTraceWithUnknownHeader_args() { ANNOTATION_PROCESSING_ERROR_HEADERS[0].replaceAll("uncaught", "undandled")), Arguments.of( "modified out of resources error header", - SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[0].replaceAll("resources", "memory"))); + SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[0].replaceAll("resources", "memory")), + Arguments.of("modified I/O error header", IO_ERROR_HEADERS[0].replaceAll("input/output", "I/O"))); } @ParameterizedTest(name = "{0}") @@ -929,6 +930,53 @@ private static Stream testSystemOutOfResourcesError_args() { Arguments.of("JDK 21 German", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[6])); } + @ParameterizedTest(name = "{0}") + @MethodSource("testIOError_args") + public void testIOError(String jdkAndLocale, String stackTraceHeader) throws Exception { + String stackTraceWithHeader = UNIDENTIFIED_LOG_LINES + stackTraceHeader + stackTraceIOError; + + List compilerMessages = + JavacCompiler.parseModernStream(4, new BufferedReader(new StringReader(stackTraceWithHeader))); + + assertThat(compilerMessages, notNullValue()); + assertThat(compilerMessages, hasSize(1)); + + String message = compilerMessages.get(0).getMessage().replaceAll(EOL, "\n"); + // Parser retains stack trace header + assertThat(message, startsWith(stackTraceHeader)); + assertThat(message, endsWith(stackTraceIOError)); + } + + private static final String stackTraceIOError = + "An input/output error occurred.\n" + "Consult the following stack trace for details.\n" + + "java.nio.charset.MalformedInputException: Input length = 1\n" + + "\tat java.base/java.nio.charset.CoderResult.throwException(CoderResult.java:274)\n" + + "\tat java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)\n" + + "\tat java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)\n" + + "\tat java.base/java.io.InputStreamReader.read(InputStreamReader.java:185)\n" + + "\tat java.base/java.io.BufferedReader.fill(BufferedReader.java:161)\n" + + "\tat java.base/java.io.BufferedReader.read(BufferedReader.java:182)\n" + + "\tat jdk.compiler/com.sun.tools.javac.main.CommandLine$Tokenizer.(CommandLine.java:143)\n" + + "\tat jdk.compiler/com.sun.tools.javac.main.CommandLine.loadCmdFile(CommandLine.java:129)\n" + + "\tat jdk.compiler/com.sun.tools.javac.main.CommandLine.appendParsedCommandArgs(CommandLine.java:71)\n" + + "\tat jdk.compiler/com.sun.tools.javac.main.CommandLine.parse(CommandLine.java:102)\n" + + "\tat jdk.compiler/com.sun.tools.javac.main.CommandLine.parse(CommandLine.java:123)\n" + + "\tat jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:215)\n" + + "\tat jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:170)\n" + + "\tat jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:57)\n" + + "\tat jdk.compiler/com.sun.tools.javac.Main.main(Main.java:43)\n"; + + private static Stream testIOError_args() { + return Stream.of( + Arguments.of("JDK 8 English", IO_ERROR_HEADERS[0]), + Arguments.of("JDK 8 Japanese", IO_ERROR_HEADERS[1]), + Arguments.of("JDK 8 Chinese", IO_ERROR_HEADERS[2]), + Arguments.of("JDK 21 English", IO_ERROR_HEADERS[3]), + Arguments.of("JDK 21 Japanese", IO_ERROR_HEADERS[4]), + Arguments.of("JDK 21 Chinese", IO_ERROR_HEADERS[5]), + Arguments.of("JDK 21 German", IO_ERROR_HEADERS[6])); + } + @Test public void testNonAnchoredWarning() throws IOException { final String error = "warning: [options] bootstrap class path not set in conjunction with -source 1.6" + EOL From 5c6eafdef33935bf016307ea76c78c3107e8f487 Mon Sep 17 00:00:00 2001 From: Alexander Kriegisch Date: Tue, 26 Dec 2023 10:50:39 +0700 Subject: [PATCH 5/6] Recognise javac.msg.plugin.uncaught.exception error header "A plugin threw an uncaught exception. Consult the following stack trace for details." --- .../plexus/compiler/javac/JavacCompiler.java | 18 +++++++- .../javac/ErrorMessageParserTest.java | 44 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java index 823ea36a..bdd6d212 100644 --- a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java +++ b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java @@ -197,6 +197,17 @@ protected static class Messages { "\n\nEin Eingabe-/Ausgabefehler ist aufgetreten.\nDetails finden Sie im folgenden Stacktrace.\n" }; + // javac.properties-> javac.msg.plugin.uncaught.exception + // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21) + protected static final String[] PLUGIN_ERROR_HEADERS = { + "\n\nA plugin threw an uncaught exception.\nConsult the following stack trace for details.\n", + "\n\nプラグインで捕捉されない例外がスローされました。\n詳細は次のスタック・トレースで調査してください。\n", + "\n\n插件抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n", + "\n\nA plugin threw an uncaught exception.\nConsult the following stack trace for details.\n", + "\n\nプラグインで捕捉されない例外がスローされました。\n詳細は次のスタック・トレースで調査してください。\n", + "\n\n插件抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n", + "\n\nEin Plug-in hat eine nicht abgefangene Ausnahme ausgel\u00F6st.\nDetails finden Sie im folgenden Stacktrace.\n" + }; } private static final Object LOCK = new Object(); @@ -782,7 +793,8 @@ static List parseModernStream(int exitCode, BufferedReader inpu || (cleanedUpMessage = getFileABugError(bufferContent)) != null || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null || (cleanedUpMessage = getSystemOutOfResourcesError(bufferContent)) != null - || (cleanedUpMessage = getIOError(bufferContent)) != null) { + || (cleanedUpMessage = getIOError(bufferContent)) != null + || (cleanedUpMessage = getPluginError(bufferContent)) != null) { errors.add(new CompilerMessage(cleanedUpMessage, ERROR)); } else if (hasPointer) { // A compiler message remains in buffer at end of parse stream @@ -851,6 +863,10 @@ private static String getIOError(String message) { return getTextStartingWithPrefix(message, IO_ERROR_HEADERS); } + private static String getPluginError(String message) { + return getTextStartingWithPrefix(message, PLUGIN_ERROR_HEADERS); + } + private static boolean startsWithPrefix(String text, String[] prefixes) { for (String prefix : prefixes) { if (text.startsWith(prefix)) { diff --git a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java index 1b7ca20b..8ac5ddff 100644 --- a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java +++ b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java @@ -812,7 +812,9 @@ private static Stream testStackTraceWithUnknownHeader_args() { Arguments.of( "modified out of resources error header", SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS[0].replaceAll("resources", "memory")), - Arguments.of("modified I/O error header", IO_ERROR_HEADERS[0].replaceAll("input/output", "I/O"))); + Arguments.of("modified I/O error header", IO_ERROR_HEADERS[0].replaceAll("input/output", "I/O")), + Arguments.of( + "modified plugin error header", PLUGIN_ERROR_HEADERS[0].replaceAll("uncaught", "unhandled"))); } @ParameterizedTest(name = "{0}") @@ -977,6 +979,46 @@ private static Stream testIOError_args() { Arguments.of("JDK 21 German", IO_ERROR_HEADERS[6])); } + @ParameterizedTest(name = "{0}") + @MethodSource("testPluginError_args") + public void testPluginError(String jdkAndLocale, String stackTraceHeader) throws Exception { + String stackTraceWithHeader = UNIDENTIFIED_LOG_LINES + stackTraceHeader + stackTracePluginError; + + List compilerMessages = + JavacCompiler.parseModernStream(4, new BufferedReader(new StringReader(stackTraceWithHeader))); + + assertThat(compilerMessages, notNullValue()); + assertThat(compilerMessages, hasSize(1)); + + String message = compilerMessages.get(0).getMessage().replaceAll(EOL, "\n"); + // Parser retains stack trace header + assertThat(message, startsWith(stackTraceHeader)); + assertThat(message, endsWith(stackTracePluginError)); + } + + private static final String stackTracePluginError = + "A plugin threw an uncaught exception.\n" + "Consult the following stack trace for details.\n" + + "java.lang.NoSuchMethodError: com.sun.tools.javac.util.JavacMessages.add(Lcom/sun/tools/javac/util/JavacMessages$ResourceBundleHelper;)V\n" + + "\tat com.google.errorprone.BaseErrorProneJavaCompiler.setupMessageBundle(BaseErrorProneJavaCompiler.java:202)\n" + + "\tat com.google.errorprone.ErrorProneJavacPlugin.init(ErrorProneJavacPlugin.java:40)\n" + + "\tat com.sun.tools.javac.main.Main.compile(Main.java:470)\n" + + "\tat com.sun.tools.javac.main.Main.compile(Main.java:381)\n" + + "\tat com.sun.tools.javac.main.Main.compile(Main.java:370)\n" + + "\tat com.sun.tools.javac.main.Main.compile(Main.java:361)\n" + + "\tat com.sun.tools.javac.Main.compile(Main.java:56)\n" + + "\tat com.sun.tools.javac.Main.main(Main.java:42)\n"; + + private static Stream testPluginError_args() { + return Stream.of( + Arguments.of("JDK 8 English", PLUGIN_ERROR_HEADERS[0]), + Arguments.of("JDK 8 Japanese", PLUGIN_ERROR_HEADERS[1]), + Arguments.of("JDK 8 Chinese", PLUGIN_ERROR_HEADERS[2]), + Arguments.of("JDK 21 English", PLUGIN_ERROR_HEADERS[3]), + Arguments.of("JDK 21 Japanese", PLUGIN_ERROR_HEADERS[4]), + Arguments.of("JDK 21 Chinese", PLUGIN_ERROR_HEADERS[5]), + Arguments.of("JDK 21 German", PLUGIN_ERROR_HEADERS[6])); + } + @Test public void testNonAnchoredWarning() throws IOException { final String error = "warning: [options] bootstrap class path not set in conjunction with -source 1.6" + EOL From f4541e88b5888d36e2df2864f8898ea4de257394 Mon Sep 17 00:00:00 2001 From: Alexander Kriegisch Date: Wed, 27 Dec 2023 12:35:32 +0700 Subject: [PATCH 6/6] Remove "error: " prefixes from javac error messages Before, "warning: " prefixes were already removed. This change brings both warnings and errors in sync with regard to message processing. Some tests had to be slightly adjusted to reflect the now cleaner error messages. --- .../plexus/compiler/javac/JavacCompiler.java | 27 +++++++++++++------ .../javac/ErrorMessageParserTest.java | 8 +++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java index bdd6d212..8c20da68 100644 --- a/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java +++ b/plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java @@ -969,11 +969,14 @@ static CompilerMessage parseModernError(int exitCode, String error) { final StringBuilder msgBuffer = new StringBuilder(); String msg = tokens.nextToken(EOL).substring(2); - // Remove the 'warning: ' prefix - final String warnPrefix = getWarningPrefix(msg); - if (warnPrefix != null) { + // Remove "error: " and "warning: " prefixes + String prefix; + if ((prefix = getErrorPrefix(msg)) != null) { + messageKind = ERROR; + msg = msg.substring(prefix.length()); + } else if ((prefix = getWarningPrefix(msg)) != null) { messageKind = WARNING; - msg = msg.substring(warnPrefix.length()); + msg = msg.substring(prefix.length()); } msgBuffer.append(msg).append(EOL); @@ -1012,15 +1015,23 @@ static CompilerMessage parseModernError(int exitCode, String error) { } } - private static String getWarningPrefix(String msg) { - for (String warningPrefix : WARNING_PREFIXES) { - if (msg.startsWith(warningPrefix)) { - return warningPrefix; + private static String getMessagePrefix(String message, String[] prefixes) { + for (String prefix : prefixes) { + if (message.startsWith(prefix)) { + return prefix; } } return null; } + private static String getWarningPrefix(String message) { + return getMessagePrefix(message, WARNING_PREFIXES); + } + + private static String getErrorPrefix(String message) { + return getMessagePrefix(message, ERROR_PREFIXES); + } + /** * put args into a temp file to be referenced using the @ option in javac command line * diff --git a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java index 8ac5ddff..ad707f4e 100644 --- a/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java +++ b/plexus-compilers/plexus-compiler-javac/src/test/java/org/codehaus/plexus/compiler/javac/ErrorMessageParserTest.java @@ -753,8 +753,7 @@ public void testJava7Error() throws Exception { assertThat( message1.getMessage(), - is("error: cannot find symbol" + EOL + " symbol: class Properties" + EOL - + " location: class Error")); + is("cannot find symbol" + EOL + " symbol: class Properties" + EOL + " location: class Error")); assertThat(message1.getStartColumn(), is(16)); @@ -770,8 +769,7 @@ public void testJava7Error() throws Exception { assertThat( message2.getMessage(), - is("error: cannot find symbol" + EOL + " symbol: class Properties" + EOL - + " location: class Error")); + is("cannot find symbol" + EOL + " symbol: class Properties" + EOL + " location: class Error")); assertThat(message2.getStartColumn(), is(35)); @@ -1259,7 +1257,7 @@ public void testWarningFollowedByBadSourceFileError() throws Exception { private void validateBadSourceFile(CompilerMessage message) { assertThat("Is an Error", message.getKind(), is(CompilerMessage.Kind.ERROR)); assertThat("On Correct File", message.getFile(), is("/MTOOLCHAINS-19/src/main/java/ch/pecunifex/x/Cls1.java")); - assertThat("Message starts with access Error", message.getMessage(), startsWith("error: cannot access Cls2")); + assertThat("Message starts with access Error", message.getMessage(), startsWith("cannot access Cls2")); } private static void assertEquivalent(CompilerMessage expected, CompilerMessage actual) {