Skip to content

Commit 0b016e4

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 - Recognise "system out of resources" error header - 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 7734a9a commit 0b016e4

File tree

3 files changed

+281
-95
lines changed

3 files changed

+281
-95
lines changed

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

Lines changed: 163 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,60 @@ 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+
};
175+
176+
// javac.properties-> javac.msg.resource
177+
// (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21)
178+
protected static final String[] SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS = {
179+
"\n\nThe system is out of resources.\nConsult the following stack trace for details.\n",
180+
"\n\nシステム・リソースが不足しています。\n詳細は次のスタック・トレースで調査してください。\n",
181+
"\n\n系统资源不足。\n有关详细信息, 请参阅以下堆栈跟踪。\n",
182+
"\n\nThe system is out of resources.\nConsult the following stack trace for details.\n",
183+
"\n\nシステム・リソースが不足しています。\n詳細は次のスタックトレースで調査してください。\n",
184+
"\n\n系统资源不足。\n有关详细信息, 请参阅以下堆栈跟踪。\n",
185+
"\n\nDas System hat keine Ressourcen mehr.\nDetails finden Sie im folgenden Stacktrace.\n"
186+
};
132187
}
133188

134189
private static final Object LOCK = new Object();
@@ -674,10 +729,6 @@ private static CompilerResult compileInProcess0(Class<?> javacClass, String[] ar
674729
private static final Pattern STACK_TRACE_OTHER_LINE =
675730
Pattern.compile("^(?:Caused by:\\s.*|\\s*at .*|\\s*\\.\\.\\.\\s\\d+\\smore)$");
676731

677-
// Match generic javac errors with 'javac:' prefix, JMV init and boot layer init errors
678-
private static final Pattern JAVAC_OR_JVM_ERROR =
679-
Pattern.compile("^(?:javac:|Error occurred during initialization of (?:boot layer|VM)).*", Pattern.DOTALL);
680-
681732
/**
682733
* Parse the compiler output into a list of compiler messages
683734
*
@@ -736,73 +787,136 @@ static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader inpu
736787
}
737788
}
738789

790+
String bufferContent = buffer.toString();
791+
if (bufferContent.isEmpty()) {
792+
return errors;
793+
}
794+
739795
// javac output not detected by other parsing
740796
// maybe better to ignore only the summary and mark the rest as error
741-
String bufferAsString = buffer.toString();
742-
if (!bufferAsString.isEmpty()) {
743-
if (JAVAC_OR_JVM_ERROR.matcher(bufferAsString).matches()) {
744-
errors.add(new CompilerMessage(bufferAsString, ERROR));
745-
} else if (hasPointer) {
746-
// A compiler message remains in buffer at end of parse stream
747-
errors.add(parseModernError(exitCode, bufferAsString));
748-
} else if (stackTraceLineCount > 0) {
749-
// Extract stack trace from end of buffer
750-
String[] lines = bufferAsString.split("\\R");
751-
int linesTotal = lines.length;
752-
buffer = new StringBuilder();
753-
int firstLine = linesTotal - stackTraceLineCount;
754-
755-
// Salvage Javac localized message 'javac.msg.bug' ("An exception has occurred in the
756-
// compiler ... Please file a bug")
757-
if (firstLine > 0) {
758-
final String lineBeforeStackTrace = lines[firstLine - 1];
759-
// One of those two URL substrings should always appear, without regard to JVM locale.
760-
// TODO: Update, if the URL changes, last checked for JDK 21.
761-
if (lineBeforeStackTrace.contains("java.sun.com/webapps/bugreport")
762-
|| lineBeforeStackTrace.contains("bugreport.java.com")) {
763-
firstLine--;
764-
}
765-
}
766-
767-
// Note: For message 'javac.msg.proc.annotation.uncaught.exception' ("An annotation processor
768-
// threw an uncaught exception"), there is no locale-independent substring, and the header is
769-
// also multi-line. It was discarded in the removed method 'parseAnnotationProcessorStream',
770-
// and we continue to do so.
771-
772-
for (int i = firstLine; i < linesTotal; i++) {
773-
buffer.append(lines[i]).append(EOL);
774-
}
775-
errors.add(new CompilerMessage(buffer.toString(), ERROR));
797+
String cleanedUpMessage;
798+
if ((cleanedUpMessage = getJavacGenericError(bufferContent)) != null
799+
|| (cleanedUpMessage = getBootLayerInitError(bufferContent)) != null
800+
|| (cleanedUpMessage = getVMInitError(bufferContent)) != null
801+
|| (cleanedUpMessage = getFileABugError(bufferContent)) != null
802+
|| (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null
803+
|| (cleanedUpMessage = getSystemOutOfResourcesError(bufferContent)) != null) {
804+
errors.add(new CompilerMessage(cleanedUpMessage, ERROR));
805+
} else if (hasPointer) {
806+
// A compiler message remains in buffer at end of parse stream
807+
errors.add(parseModernError(exitCode, bufferContent));
808+
} else if (stackTraceLineCount > 0) {
809+
// Extract stack trace from end of buffer
810+
String[] lines = bufferContent.split("\\R");
811+
int linesTotal = lines.length;
812+
buffer = new StringBuilder();
813+
int firstLine = linesTotal - stackTraceLineCount;
814+
for (int i = firstLine; i < linesTotal; i++) {
815+
buffer.append(lines[i]).append(EOL);
776816
}
817+
errors.add(new CompilerMessage(buffer.toString(), ERROR));
777818
}
819+
// TODO: Add something like this? Check if it creates more value or more unnecessary log output in general.
820+
// else {
821+
// // Fall-back, if still no error or stack trace was recognised
822+
// errors.add(new CompilerMessage(bufferContent, exitCode == 0 ? OTHER : ERROR));
823+
// }
824+
778825
return errors;
779826
}
780827

781-
private static boolean isMisc(String line) {
782-
return startsWithPrefix(line, MISC_PREFIXES);
828+
private static boolean isMisc(String message) {
829+
return startsWithPrefix(message, MISC_PREFIXES);
830+
}
831+
832+
private static boolean isNote(String message) {
833+
return startsWithPrefix(message, NOTE_PREFIXES);
834+
}
835+
836+
private static boolean isWarning(String message) {
837+
return startsWithPrefix(message, WARNING_PREFIXES);
838+
}
839+
840+
private static boolean isError(String message) {
841+
return startsWithPrefix(message, ERROR_PREFIXES);
842+
}
843+
844+
private static String getJavacGenericError(String message) {
845+
return getTextStartingWithPrefix(message, JAVAC_GENERIC_ERROR_PREFIXES);
783846
}
784847

785-
private static boolean isNote(String line) {
786-
return startsWithPrefix(line, NOTE_PREFIXES);
848+
private static String getVMInitError(String message) {
849+
return getTextStartingWithPrefix(message, VM_INIT_ERROR_HEADERS);
787850
}
788851

789-
private static boolean isWarning(String line) {
790-
return startsWithPrefix(line, WARNING_PREFIXES);
852+
private static String getBootLayerInitError(String message) {
853+
return getTextStartingWithPrefix(message, BOOT_LAYER_INIT_ERROR_HEADERS);
791854
}
792855

793-
private static boolean isError(String line) {
794-
return startsWithPrefix(line, ERROR_PREFIXES);
856+
private static String getFileABugError(String message) {
857+
return getTextStartingWithPrefix(message, FILE_A_BUG_ERROR_HEADERS);
795858
}
796859

797-
private static boolean startsWithPrefix(String line, String[] prefixes) {
860+
private static String getAnnotationProcessingError(String message) {
861+
return getTextStartingWithPrefix(message, ANNOTATION_PROCESSING_ERROR_HEADERS);
862+
}
863+
864+
private static String getSystemOutOfResourcesError(String message) {
865+
return getTextStartingWithPrefix(message, SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS);
866+
}
867+
868+
private static boolean startsWithPrefix(String text, String[] prefixes) {
798869
for (String prefix : prefixes) {
799-
if (line.startsWith(prefix)) {
870+
if (text.startsWith(prefix)) {
800871
return true;
801872
}
802873
}
803874
return false;
804875
}
805876

877+
/**
878+
* Identify and return a known javac error message prefix and all subsequent text - usually a stack trace - from a
879+
* javac log output buffer.
880+
*
881+
* @param text log buffer to search for a javac error message stack trace
882+
* @param prefixes array of strings in Java properties format, e.g. {@code "some error with line feed\nand parameter
883+
* placeholders {0} and {1}"} in multiple locales (hence the array). For the search, the
884+
* placeholders may be represented by any text in the log buffer.
885+
* @return if found, the error message + all subsequent text, otherwise {@code null}
886+
*/
887+
static String getTextStartingWithPrefix(String text, String[] prefixes) {
888+
// Implementation note: The properties format with placeholders makes it easy to just copy & paste values from
889+
// the JDK compared to having to convert them to regular expressions with ".*" instead of "{0}" and quote
890+
// special regex characters. This makes the implementation of this method more complex and potentially a bit
891+
// slower, but hopefully is worth the effort for the convenience of future developers maintaining this class.
892+
893+
// Normalise line feeds to the UNIX format found in JDK multi-line messages in properties files
894+
text = text.replaceAll("\\R", "\n");
895+
896+
// Search text for given error message prefixes/headers, until the first match is found
897+
for (String prefix : prefixes) {
898+
// Split properties message along placeholders like "{0}", "{1}" etc.
899+
String[] prefixParts = prefix.split("\\{\\d+\\}");
900+
for (int i = 0; i < prefixParts.length; i++) {
901+
// Make sure to treat split sections as literal text in search regex by enclosing them in "\Q" and "\E".
902+
// See https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html, search for "Quotation".
903+
prefixParts[i] = "\\Q" + prefixParts[i] + "\\E";
904+
}
905+
// Join message parts, replacing properties placeholders by ".*" regex ones
906+
prefix = String.join(".*?", prefixParts);
907+
// Find prefix + subsequent text in Pattern.DOTALL mode, represented in regex as "(?s)".
908+
// This matches across line break boundaries.
909+
Matcher matcher = Pattern.compile("(?s).*(" + prefix + ".*)").matcher(text);
910+
if (matcher.matches()) {
911+
// Match -> cut off text before header and replace UNIX line breaks by platform ones again
912+
return matcher.replaceFirst("$1").replaceAll("\n", EOL);
913+
}
914+
}
915+
916+
// No match
917+
return null;
918+
}
919+
806920
/**
807921
* Construct a compiler message object from a compiler output line
808922
*

0 commit comments

Comments
 (0)