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..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 @@ -62,9 +62,11 @@ 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; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.codehaus.plexus.compiler.AbstractCompiler; @@ -80,10 +82,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 +97,123 @@ @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 "}; - - // see compiler.note.note in compiler.properties of javac sources - private static final String[] NOTE_PREFIXES = {"Note: ", "\u6ce8: ", "\u6ce8\u610f\uff1a "}; - - // see compiler.misc.verbose in compiler.properties of javac sources - private static final String[] MISC_PREFIXES = {"["}; + /** + * 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: "}; + + // compiler.properties -> compiler.note.note (en, ja, zh_CN, de) + protected static final String[] NOTE_PREFIXES = {"Note: ", "ノート: ", "注: ", "Hinweis: "}; + + // 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" + }; + + // 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" + }; + + // 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" + }; + + // 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(); - private static final String JAVAC_CLASSNAME = "com.sun.tools.javac.Main"; private volatile Class javacClass; - private final Deque> javacClasses = new ConcurrentLinkedDeque<>(); @Inject @@ -131,13 +239,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 +251,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 +265,6 @@ public CompilerResult performCompile(CompilerConfiguration config) throws Compil executable = "javac"; } } - result = compileOutOfProcess(config, executable, args); } else { if (isJava16() && !config.isForceJavacCompilerUse()) { @@ -200,9 +303,7 @@ public static String[] buildCompilerArguments(CompilerConfiguration config, Stri // ---------------------------------------------------------------------- File destinationDir = new File(config.getOutputLocation()); - args.add("-d"); - args.add(destinationDir.getAbsolutePath()); // ---------------------------------------------------------------------- @@ -212,14 +313,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 +327,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 +338,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 +352,6 @@ public static String[] buildCompilerArguments(CompilerConfiguration config, Stri if (i > 0) { buffer.append(","); } - buffer.append(procs[i]); } args.add(buffer.toString()); @@ -366,13 +462,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 +506,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 +528,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 +548,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 +596,6 @@ protected CompilerResult compileOutOfProcess(CompilerConfiguration config, Strin Commandline cli = new Commandline(); cli.setWorkingDirectory(config.getWorkingDirectory().getAbsolutePath()); - cli.setExecutable(executable); try { @@ -522,7 +607,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 +621,7 @@ protected CompilerResult compileOutOfProcess(CompilerConfiguration config, Strin } CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer(); - int returnCode; - List messages; if (getLog().isDebugEnabled()) { @@ -563,6 +645,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 +696,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); @@ -638,74 +721,22 @@ 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 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,46 +748,128 @@ 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; } } + + 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 cleanedUpMessage; + if ((cleanedUpMessage = getJavacGenericError(bufferContent)) != null + || (cleanedUpMessage = getBootLayerInitError(bufferContent)) != null + || (cleanedUpMessage = getVMInitError(bufferContent)) != null + || (cleanedUpMessage = getFileABugError(bufferContent)) != null + || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null + || (cleanedUpMessage = getSystemOutOfResourcesError(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 + 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 message) { + return startsWithPrefix(message, MISC_PREFIXES); } - private static boolean isMisc(String line) { - return startsWithPrefix(line, MISC_PREFIXES); + private static boolean isNote(String message) { + return startsWithPrefix(message, NOTE_PREFIXES); } - private static boolean isNote(String line) { - return startsWithPrefix(line, NOTE_PREFIXES); + private static boolean isWarning(String message) { + return startsWithPrefix(message, WARNING_PREFIXES); } - private static boolean startsWithPrefix(String line, String[] prefixes) { + private static boolean isError(String message) { + return startsWithPrefix(message, ERROR_PREFIXES); + } + + private static String getJavacGenericError(String message) { + return getTextStartingWithPrefix(message, JAVAC_GENERIC_ERROR_PREFIXES); + } + + private static String getVMInitError(String message) { + return getTextStartingWithPrefix(message, VM_INIT_ERROR_HEADERS); + } + + private static String getBootLayerInitError(String message) { + return getTextStartingWithPrefix(message, BOOT_LAYER_INIT_ERROR_HEADERS); + } + + 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 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 String getPluginError(String message) { + return getTextStartingWithPrefix(message, PLUGIN_ERROR_HEADERS); + } + + private static boolean startsWithPrefix(String text, String[] prefixes) { for (String prefix : prefixes) { - if (line.startsWith(prefix)) { + if (text.startsWith(prefix)) { return true; } } @@ -764,26 +877,66 @@ private static boolean startsWithPrefix(String line, String[] prefixes) { } /** - * Construct a CompilerMessage object from a line of the compiler output + * 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 * - * @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 +949,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 +960,39 @@ 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); - if (warnPrefix != null) { - isError = false; - msg = msg.substring(warnPrefix.length()); - } else { - isError = exitCode != 0; + // 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(prefix.length()); } - - 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,32 +1000,38 @@ 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) { - 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 * @@ -905,15 +1051,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 +1076,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 +1100,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); } 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..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 @@ -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 { @@ -751,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)); @@ -768,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)); @@ -780,10 +780,45 @@ 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")), + 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 plugin error header", PLUGIN_ERROR_HEADERS[0].replaceAll("uncaught", "unhandled"))); + } + @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 +833,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 +868,153 @@ 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"))); + } + + @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])); + } + + @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])); + } + + @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 @@ -1011,28 +1173,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 @@ -1093,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) { 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])); } }