Skip to content

Commit 6c20fa8

Browse files
committed
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
1 parent b658d3e commit 6c20fa8

File tree

3 files changed

+211
-95
lines changed

3 files changed

+211
-95
lines changed

plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac/JavacCompiler.java

Lines changed: 146 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import java.util.Properties;
6767
import java.util.StringTokenizer;
6868
import java.util.concurrent.ConcurrentLinkedDeque;
69+
import java.util.regex.Matcher;
6970
import java.util.regex.Pattern;
7071

7172
import org.codehaus.plexus.compiler.AbstractCompiler;
@@ -129,6 +130,48 @@ protected static class Messages {
129130

130131
// compiler.properties -> compiler.misc.verbose.*
131132
protected static final String[] MISC_PREFIXES = {"["};
133+
134+
// Generic javac error prefix
135+
// TODO: In JDK 8, this generic prefix no longer seems to be in use for javac error messages, at least not in
136+
// the Java part of javac. Maybe in C sources? Does javac even use any native classes?
137+
protected static final String[] JAVAC_GENERIC_ERROR_PREFIXES = {"javac:"};
138+
139+
// Hard-coded, English-only error header in JVM native code, *not* followed by stack trace, but rather
140+
// by another text message
141+
protected static final String[] VM_INIT_ERROR_HEADERS = {"Error occurred during initialization of VM"};
142+
143+
// Hard-coded, English-only error header in class System, followed by stack trace
144+
protected static final String[] BOOT_LAYER_INIT_ERROR_HEADERS = {
145+
"Error occurred during initialization of boot layer"
146+
};
147+
148+
// javac.properties-> javac.msg.proc.annotation.uncaught.exception
149+
// (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21)
150+
protected static final String[] ANNOTATION_PROCESSING_ERROR_HEADERS = {
151+
"\n\nAn annotation processor threw an uncaught exception.\nConsult the following stack trace for details.\n\n",
152+
"\n\n注釈処理で捕捉されない例外がスローされました。\n詳細は次のスタック・トレースで調査してください。\n\n",
153+
"\n\n批注处理程序抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n\n",
154+
"\n\nAn annotation processor threw an uncaught exception.\nConsult the following stack trace for details.\n\n",
155+
"\n\n注釈処理で捕捉されない例外がスローされました。\n詳細は次のスタックトレースで調査してください。\n\n",
156+
"\n\n批注处理程序抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n\n",
157+
"\n\nEin Annotationsprozessor hat eine nicht abgefangene Ausnahme ausgelöst.\nDetails finden Sie im folgenden Stacktrace.\n\n"
158+
};
159+
160+
// javac.properties-> javac.msg.bug
161+
// (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
162+
// JDK-21)
163+
protected static final String[] FILE_A_BUG_ERROR_HEADERS = {
164+
"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",
165+
"コンパイラで例外が発生しました({0})。Bug Paradeで重複がないかをご確認のうえ、Java Developer Connection (http://java.sun.com/webapps/bugreport)でbugの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を含めてください。ご協力ありがとうございます。\n",
166+
"编译器 ({0}) 中出现异常错误。 如果在 Bug Parade 中没有找到该错误, 请在 Java Developer Connection (http://java.sun.com/webapps/bugreport) 中建立 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。\n",
167+
"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.",
168+
"コンパイラで例外が発生しました({0})。Bug Database (http://bugs.java.com)で重複がないかをご確認のうえ、Java bugレポート・ページ(http://bugreport.java.com)でJavaコンパイラに対するbugの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を含めてください。ご協力ありがとうございます。",
169+
"编译器 ({0}) 中出现异常错误。如果在 Bug Database (http://bugs.java.com) 中没有找到该错误, 请通过 Java Bug 报告页 (http://bugreport.java.com) 建立该 Java 编译器 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。",
170+
"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",
171+
"コンパイラで例外が発生しました({0})。バグ・データベース(https://bugs.java.com)で重複がないかをご確認のうえ、Javaのバグ・レポート・ページ(https://bugreport.java.com)から、Javaコンパイラに対するバグの登録をお願いいたします。レポートには、該当のプログラム、次の診断内容、およびJavaコンパイラに渡されたパラメータをご入力ください。ご協力ありがとうございます。\n",
172+
"编译器 ({0}) 中出现异常错误。如果在 Bug Database (https://bugs.java.com) 中没有找到有关该错误的 Java 编译器 Bug,请通过 Java Bug 报告页 (https://bugreport.java.com) 提交 Java 编译器 Bug。请在报告中附上您的程序、以下诊断信息以及传递到 Java 编译器的参数。谢谢。\n",
173+
"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"
174+
};
132175
}
133176

134177
private static final Object LOCK = new Object();
@@ -642,10 +685,6 @@ private static CompilerResult compileInProcess0(Class<?> javacClass, String[] ar
642685
private static final Pattern STACK_TRACE_OTHER_LINE =
643686
Pattern.compile("^(?:Caused by:\\s.*|\\s*at .*|\\s*\\.\\.\\.\\s\\d+\\smore)$");
644687

645-
// Match generic javac errors with 'javac:' prefix, JMV init and boot layer init errors
646-
private static final Pattern JAVAC_OR_JVM_ERROR =
647-
Pattern.compile("^(?:javac:|Error occurred during initialization of (?:boot layer|VM)).*", Pattern.DOTALL);
648-
649688
/**
650689
* Parse the compiler output into a list of compiler messages
651690
*
@@ -704,73 +743,131 @@ static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader inpu
704743
}
705744
}
706745

746+
String bufferContent = buffer.toString();
747+
if (bufferContent.isEmpty()) {
748+
return errors;
749+
}
750+
707751
// javac output not detected by other parsing
708752
// maybe better to ignore only the summary and mark the rest as error
709-
String bufferAsString = buffer.toString();
710-
if (!bufferAsString.isEmpty()) {
711-
if (JAVAC_OR_JVM_ERROR.matcher(bufferAsString).matches()) {
712-
errors.add(new CompilerMessage(bufferAsString, ERROR));
713-
} else if (hasPointer) {
714-
// A compiler message remains in buffer at end of parse stream
715-
errors.add(parseModernError(exitCode, bufferAsString));
716-
} else if (stackTraceLineCount > 0) {
717-
// Extract stack trace from end of buffer
718-
String[] lines = bufferAsString.split("\\R");
719-
int linesTotal = lines.length;
720-
buffer = new StringBuilder();
721-
int firstLine = linesTotal - stackTraceLineCount;
722-
723-
// Salvage Javac localized message 'javac.msg.bug' ("An exception has occurred in the
724-
// compiler ... Please file a bug")
725-
if (firstLine > 0) {
726-
final String lineBeforeStackTrace = lines[firstLine - 1];
727-
// One of those two URL substrings should always appear, without regard to JVM locale.
728-
// TODO: Update, if the URL changes, last checked for JDK 21.
729-
if (lineBeforeStackTrace.contains("java.sun.com/webapps/bugreport")
730-
|| lineBeforeStackTrace.contains("bugreport.java.com")) {
731-
firstLine--;
732-
}
733-
}
734-
735-
// Note: For message 'javac.msg.proc.annotation.uncaught.exception' ("An annotation processor
736-
// threw an uncaught exception"), there is no locale-independent substring, and the header is
737-
// also multi-line. It was discarded in the removed method 'parseAnnotationProcessorStream',
738-
// and we continue to do so.
739-
740-
for (int i = firstLine; i < linesTotal; i++) {
741-
buffer.append(lines[i]).append(EOL);
742-
}
743-
errors.add(new CompilerMessage(buffer.toString(), ERROR));
753+
String cleanedUpMessage;
754+
if ((cleanedUpMessage = getJavacGenericError(bufferContent)) != null
755+
|| (cleanedUpMessage = getBootLayerInitError(bufferContent)) != null
756+
|| (cleanedUpMessage = getVMInitError(bufferContent)) != null
757+
|| (cleanedUpMessage = getFileABugError(bufferContent)) != null
758+
|| (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null) {
759+
errors.add(new CompilerMessage(cleanedUpMessage, ERROR));
760+
} else if (hasPointer) {
761+
// A compiler message remains in buffer at end of parse stream
762+
errors.add(parseModernError(exitCode, bufferContent));
763+
} else if (stackTraceLineCount > 0) {
764+
// Extract stack trace from end of buffer
765+
String[] lines = bufferContent.split("\\R");
766+
int linesTotal = lines.length;
767+
buffer = new StringBuilder();
768+
int firstLine = linesTotal - stackTraceLineCount;
769+
for (int i = firstLine; i < linesTotal; i++) {
770+
buffer.append(lines[i]).append(EOL);
744771
}
772+
errors.add(new CompilerMessage(buffer.toString(), ERROR));
745773
}
774+
// TODO: Add something like this? Check if it creates more value or more unnecessary log output in general.
775+
// else {
776+
// // Fall-back, if still no error or stack trace was recognised
777+
// errors.add(new CompilerMessage(bufferContent, exitCode == 0 ? OTHER : ERROR));
778+
// }
779+
746780
return errors;
747781
}
748782

749-
private static boolean isMisc(String line) {
750-
return startsWithPrefix(line, MISC_PREFIXES);
783+
private static boolean isMisc(String message) {
784+
return startsWithPrefix(message, MISC_PREFIXES);
785+
}
786+
787+
private static boolean isNote(String message) {
788+
return startsWithPrefix(message, NOTE_PREFIXES);
789+
}
790+
791+
private static boolean isWarning(String message) {
792+
return startsWithPrefix(message, WARNING_PREFIXES);
793+
}
794+
795+
private static boolean isError(String message) {
796+
return startsWithPrefix(message, ERROR_PREFIXES);
751797
}
752798

753-
private static boolean isNote(String line) {
754-
return startsWithPrefix(line, NOTE_PREFIXES);
799+
private static String getJavacGenericError(String message) {
800+
return getTextStartingWithPrefix(message, JAVAC_GENERIC_ERROR_PREFIXES);
755801
}
756802

757-
private static boolean isWarning(String line) {
758-
return startsWithPrefix(line, WARNING_PREFIXES);
803+
private static String getVMInitError(String message) {
804+
return getTextStartingWithPrefix(message, VM_INIT_ERROR_HEADERS);
759805
}
760806

761-
private static boolean isError(String line) {
762-
return startsWithPrefix(line, ERROR_PREFIXES);
807+
private static String getBootLayerInitError(String message) {
808+
return getTextStartingWithPrefix(message, BOOT_LAYER_INIT_ERROR_HEADERS);
763809
}
764810

765-
private static boolean startsWithPrefix(String line, String[] prefixes) {
811+
private static String getFileABugError(String message) {
812+
return getTextStartingWithPrefix(message, FILE_A_BUG_ERROR_HEADERS);
813+
}
814+
815+
private static String getAnnotationProcessingError(String message) {
816+
return getTextStartingWithPrefix(message, ANNOTATION_PROCESSING_ERROR_HEADERS);
817+
}
818+
819+
private static boolean startsWithPrefix(String text, String[] prefixes) {
766820
for (String prefix : prefixes) {
767-
if (line.startsWith(prefix)) {
821+
if (text.startsWith(prefix)) {
768822
return true;
769823
}
770824
}
771825
return false;
772826
}
773827

828+
/**
829+
* Identify and return a known javac error message prefix and all subsequent text - usually a stack trace - from a
830+
* javac log output buffer.
831+
*
832+
* @param text log buffer to search for a javac error message stack trace
833+
* @param prefixes array of strings in Java properties format, e.g. {@code "some error with line feed\nand parameter
834+
* placeholders {0} and {1}"} in multiple locales (hence the array). For the search, the
835+
* placeholders may be represented by any text in the log buffer.
836+
* @return if found, the error message + all subsequent text, otherwise {@code null}
837+
*/
838+
static String getTextStartingWithPrefix(String text, String[] prefixes) {
839+
// Implementation note: The properties format with placeholders makes it easy to just copy & paste values from
840+
// the JDK compared to having to convert them to regular expressions with ".*" instead of "{0}" and quote
841+
// special regex characters. This makes the implementation of this method more complex and potentially a bit
842+
// slower, but hopefully is worth the effort for the convenience of future developers maintaining this class.
843+
844+
// Normalise line feeds to the UNIX format found in JDK multi-line messages in properties files
845+
text = text.replaceAll("\\R", "\n");
846+
847+
// Search text for given error message prefixes/headers, until the first match is found
848+
for (String prefix : prefixes) {
849+
// Split properties message along placeholders like "{0}", "{1}" etc.
850+
String[] prefixParts = prefix.split("\\{\\d+\\}");
851+
for (int i = 0; i < prefixParts.length; i++) {
852+
// Make sure to treat split sections as literal text in search regex by enclosing them in "\Q" and "\E".
853+
// See https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html, search for "Quotation".
854+
prefixParts[i] = "\\Q" + prefixParts[i] + "\\E";
855+
}
856+
// Join message parts, replacing properties placeholders by ".*" regex ones
857+
prefix = String.join(".*?", prefixParts);
858+
// Find prefix + subsequent text in Pattern.DOTALL mode, represented in regex as "(?s)".
859+
// This matches across line break boundaries.
860+
Matcher matcher = Pattern.compile("(?s).*(" + prefix + ".*)").matcher(text);
861+
if (matcher.matches()) {
862+
// Match -> cut off text before header and replace UNIX line breaks by platform ones again
863+
return matcher.replaceFirst("$1").replaceAll("\n", EOL);
864+
}
865+
}
866+
867+
// No match
868+
return null;
869+
}
870+
774871
/**
775872
* Construct a compiler message object from a compiler output line
776873
*

0 commit comments

Comments
 (0)