Skip to content

Commit 1e78c83

Browse files
committed
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.
1 parent e7f2762 commit 1e78c83

File tree

1 file changed

+133
-113
lines changed
  • plexus-compilers/plexus-compiler-javac/src/main/java/org/codehaus/plexus/compiler/javac

1 file changed

+133
-113
lines changed

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

+133-113
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import java.util.List;
6363
import java.util.Map;
6464
import java.util.NoSuchElementException;
65+
import java.util.Objects;
6566
import java.util.Properties;
6667
import java.util.StringTokenizer;
6768
import java.util.concurrent.ConcurrentLinkedDeque;
@@ -80,25 +81,55 @@
8081
import org.codehaus.plexus.util.cli.CommandLineUtils;
8182
import org.codehaus.plexus.util.cli.Commandline;
8283

84+
import static org.codehaus.plexus.compiler.CompilerMessage.Kind.*;
85+
import static org.codehaus.plexus.compiler.javac.JavacCompiler.Messages.*;
86+
8387
/**
8488
* @author <a href="mailto:[email protected]">Trygve Laugst&oslash;l</a>
8589
* @author <a href="mailto:[email protected]">Matthew Pocock</a>
8690
* @author <a href="mailto:[email protected]">J&ouml;rg Wa&szlig;mer</a>
91+
* @author Alexander Kriegisch
8792
* @author Others
8893
*
8994
*/
9095
@Named("javac")
9196
@Singleton
9297
public class JavacCompiler extends AbstractCompiler {
9398

94-
// see compiler.warn.warning in compiler.properties of javac sources
95-
private static final String[] WARNING_PREFIXES = {"warning: ", "\u8b66\u544a: ", "\u8b66\u544a\uff1a "};
99+
/**
100+
* Multi-language compiler messages to parse from forked javac output.
101+
* <ul>
102+
* <li>OpenJDK 8+ is delivered with 3 locales (en, ja, zh_CN).</li>
103+
* <li>OpenJDK 21+ is delivered with 4 locales (en, ja, zh_CN, de).</li>
104+
* </ul>
105+
* Instead of manually duplicating multi-language messages into this class, it would be preferable to fetch the
106+
* strings directly from the running JDK:
107+
* <pre>{@code
108+
* new JavacMessages("com.sun.tools.javac.resources.javac", Locale.getDefault())
109+
* .getLocalizedString("javac.msg.proc.annotation.uncaught.exception")
110+
* }</pre>
111+
* Hoewever, due to JMS module protection, it would be necessary to run Plexus Compiler (and hence also Maven
112+
* Compiler and the whole Maven JVM) with {@code --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED}
113+
* on more recent JDK versions. As this cannot be reliably expected and using internal APIs - even though stable
114+
* since at least JDK 8 - it is not a future-proof approach. So we refrain from doing so, even though during Plexus
115+
* Compiler development it might come in handy.
116+
* <p>
117+
* TODO: Check compiler.properties and javac.properties in OpenJDK javac source code for
118+
* message changes, relevant new messages, new locales.
119+
*/
120+
protected static class Messages {
121+
// compiler.properties -> compiler.err.error (en, ja, zh_CN, de)
122+
protected static final String[] ERROR_PREFIXES = {"error: ", "エラー: ", "错误: ", "Fehler: "};
96123

97-
// see compiler.note.note in compiler.properties of javac sources
98-
private static final String[] NOTE_PREFIXES = {"Note: ", "\u6ce8: ", "\u6ce8\u610f\uff1a "};
124+
// compiler.properties -> compiler.warn.warning (en, ja, zh_CN, de)
125+
protected static final String[] WARNING_PREFIXES = {"warning: ", "警告: ", "警告: ", "Warnung: "};
99126

100-
// see compiler.misc.verbose in compiler.properties of javac sources
101-
private static final String[] MISC_PREFIXES = {"["};
127+
// compiler.properties -> compiler.note.note (en, ja, zh_CN, de)
128+
protected static final String[] NOTE_PREFIXES = {"Note: ", "ノート: ", "注: ", "Hinweis: "};
129+
130+
// compiler.properties -> compiler.misc.verbose.*
131+
protected static final String[] MISC_PREFIXES = {"["};
132+
}
102133

103134
private static final Object LOCK = new Object();
104135

@@ -563,6 +594,11 @@ protected CompilerResult compileOutOfProcess(CompilerConfiguration config, Strin
563594
}
564595

565596
try {
597+
// TODO:
598+
// Is it really helpful to parse stdOut and stdErr as a single stream, instead of taking the chance to
599+
// draw extra information from the fact that normal javac output is written to stdOut, while warnings and
600+
// errors are written to stdErr? Of course, chronological correlation of messages would be more difficult
601+
// then, but basically, we are throwing away information here.
566602
returnCode = CommandLineUtils.executeCommandLine(cli, out, out);
567603

568604
messages = parseModernStream(returnCode, new BufferedReader(new StringReader(out.getOutput())));
@@ -643,69 +679,21 @@ private static CompilerResult compileInProcess0(Class<?> javacClass, String[] ar
643679
Pattern.compile("^(?:javac:|Error occurred during initialization of (?:boot layer|VM)).*", Pattern.DOTALL);
644680

645681
/**
646-
* Parse the output from the compiler into a list of CompilerMessage objects
682+
* Parse the compiler output into a list of compiler messages
647683
*
648-
* @param exitCode The exit code of javac.
649-
* @param input The output of the compiler
650-
* @return List of CompilerMessage objects
651-
* @throws IOException
684+
* @param exitCode javac exit code (0 on success, non-zero otherwise)
685+
* @param input compiler output (stdOut and stdErr merged into input stream)
686+
* @return list of {@link CompilerMessage} objects
687+
* @throws IOException if there is a problem reading from the input reader
652688
*/
653689
static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader input) throws IOException {
654690
List<CompilerMessage> errors = new ArrayList<>();
655-
656691
String line;
657-
658692
StringBuilder buffer = new StringBuilder();
659-
660693
boolean hasPointer = false;
661694
int stackTraceLineCount = 0;
662695

663-
while (true) {
664-
line = input.readLine();
665-
666-
if (line == null) {
667-
// javac output not detected by other parsing
668-
// maybe better to ignore only the summary and mark the rest as error
669-
String bufferAsString = buffer.toString();
670-
if (buffer.length() > 0) {
671-
if (JAVAC_OR_JVM_ERROR.matcher(bufferAsString).matches()) {
672-
errors.add(new CompilerMessage(bufferAsString, CompilerMessage.Kind.ERROR));
673-
} else if (hasPointer) {
674-
// A compiler message remains in buffer at end of parse stream
675-
errors.add(parseModernError(exitCode, bufferAsString));
676-
} else if (stackTraceLineCount > 0) {
677-
// Extract stack trace from end of buffer
678-
String[] lines = bufferAsString.split("\\R");
679-
int linesTotal = lines.length;
680-
buffer = new StringBuilder();
681-
int firstLine = linesTotal - stackTraceLineCount;
682-
683-
// Salvage Javac localized message 'javac.msg.bug' ("An exception has occurred in the
684-
// compiler ... Please file a bug")
685-
if (firstLine > 0) {
686-
final String lineBeforeStackTrace = lines[firstLine - 1];
687-
// One of those two URL substrings should always appear, without regard to JVM locale.
688-
// TODO: Update, if the URL changes, last checked for JDK 21.
689-
if (lineBeforeStackTrace.contains("java.sun.com/webapps/bugreport")
690-
|| lineBeforeStackTrace.contains("bugreport.java.com")) {
691-
firstLine--;
692-
}
693-
}
694-
695-
// Note: For message 'javac.msg.proc.annotation.uncaught.exception' ("An annotation processor
696-
// threw an uncaught exception"), there is no locale-independent substring, and the header is
697-
// also multi-line. It was discarded in the removed method 'parseAnnotationProcessorStream',
698-
// and we continue to do so.
699-
700-
for (int i = firstLine; i < linesTotal; i++) {
701-
buffer.append(lines[i]).append(EOL);
702-
}
703-
errors.add(new CompilerMessage(buffer.toString(), CompilerMessage.Kind.ERROR));
704-
}
705-
}
706-
return errors;
707-
}
708-
696+
while ((line = input.readLine()) != null) {
709697
if (stackTraceLineCount == 0 && STACK_TRACE_FIRST_LINE.matcher(line).matches()
710698
|| STACK_TRACE_OTHER_LINE.matcher(line).matches()) {
711699
stackTraceLineCount++;
@@ -717,33 +705,77 @@ static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader inpu
717705
if (!line.startsWith(" ") && hasPointer) {
718706
// add the error bean
719707
errors.add(parseModernError(exitCode, buffer.toString()));
720-
721708
// reset for next error block
722709
buffer = new StringBuilder(); // this is quicker than clearing it
723-
724710
hasPointer = false;
725711
}
726712

727-
// TODO: there should be a better way to parse these
728-
if ((buffer.length() == 0) && line.startsWith("error: ")) {
729-
errors.add(new CompilerMessage(line, CompilerMessage.Kind.ERROR));
730-
} else if ((buffer.length() == 0) && line.startsWith("warning: ")) {
731-
errors.add(new CompilerMessage(line, CompilerMessage.Kind.WARNING));
732-
} else if ((buffer.length() == 0) && isNote(line)) {
733-
// skip, JDK 1.5 telling us deprecated APIs are used but -Xlint:deprecation isn't set
734-
} else if ((buffer.length() == 0) && isMisc(line)) {
735-
// verbose output was set
736-
errors.add(new CompilerMessage(line, CompilerMessage.Kind.OTHER));
713+
if (buffer.length() == 0) {
714+
// try to classify output line by type (error, warning etc.)
715+
// TODO: there should be a better way to parse these
716+
if (isError(line)) {
717+
errors.add(new CompilerMessage(line, ERROR));
718+
} else if (isWarning(line)) {
719+
errors.add(new CompilerMessage(line, WARNING));
720+
} else if (isNote(line)) {
721+
// skip, JDK telling us deprecated APIs are used but -Xlint:deprecation isn't set
722+
} else if (isMisc(line)) {
723+
// verbose output was set
724+
errors.add(new CompilerMessage(line, CompilerMessage.Kind.OTHER));
725+
} else {
726+
// add first unclassified line to buffer
727+
buffer.append(line).append(EOL);
728+
}
737729
} else {
738-
buffer.append(line);
739-
740-
buffer.append(EOL);
730+
// add next unclassified line to buffer
731+
buffer.append(line).append(EOL);
741732
}
742733

743734
if (line.endsWith("^")) {
744735
hasPointer = true;
745736
}
746737
}
738+
739+
// javac output not detected by other parsing
740+
// 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));
776+
}
777+
}
778+
return errors;
747779
}
748780

749781
private static boolean isMisc(String line) {
@@ -754,6 +786,14 @@ private static boolean isNote(String line) {
754786
return startsWithPrefix(line, NOTE_PREFIXES);
755787
}
756788

789+
private static boolean isWarning(String line) {
790+
return startsWithPrefix(line, WARNING_PREFIXES);
791+
}
792+
793+
private static boolean isError(String line) {
794+
return startsWithPrefix(line, ERROR_PREFIXES);
795+
}
796+
757797
private static boolean startsWithPrefix(String line, String[] prefixes) {
758798
for (String prefix : prefixes) {
759799
if (line.startsWith(prefix)) {
@@ -764,26 +804,24 @@ private static boolean startsWithPrefix(String line, String[] prefixes) {
764804
}
765805

766806
/**
767-
* Construct a CompilerMessage object from a line of the compiler output
807+
* Construct a compiler message object from a compiler output line
768808
*
769-
* @param exitCode The exit code from javac.
770-
* @param error output line from the compiler
771-
* @return the CompilerMessage object
809+
* @param exitCode javac exit code
810+
* @param error compiler output line
811+
* @return compiler message object
772812
*/
773813
static CompilerMessage parseModernError(int exitCode, String error) {
774814
final StringTokenizer tokens = new StringTokenizer(error, ":");
775815

776-
boolean isError = exitCode != 0;
816+
CompilerMessage.Kind messageKind = exitCode == 0 ? WARNING : ERROR;
777817

778818
try {
779819
// With Java 6 error output lines from the compiler got longer. For backward compatibility
780-
// .. and the time being, we eat up all (if any) tokens up to the erroneous file and source
781-
// .. line indicator tokens.
820+
// and the time being, we eat up all (if any) tokens up to the erroneous file and source
821+
// line indicator tokens.
782822

783823
boolean tokenIsAnInteger;
784-
785824
StringBuilder file = null;
786-
787825
String currentToken = null;
788826

789827
do {
@@ -796,9 +834,7 @@ static CompilerMessage parseModernError(int exitCode, String error) {
796834
}
797835

798836
currentToken = tokens.nextToken();
799-
800837
// Probably the only backward compatible means of checking if a string is an integer.
801-
802838
tokenIsAnInteger = true;
803839

804840
try {
@@ -809,75 +845,59 @@ static CompilerMessage parseModernError(int exitCode, String error) {
809845
} while (!tokenIsAnInteger);
810846

811847
final String lineIndicator = currentToken;
812-
813-
final int startOfFileName = file.toString().lastIndexOf(']');
814-
848+
final int startOfFileName = Objects.requireNonNull(file).toString().lastIndexOf(']');
815849
if (startOfFileName > -1) {
816850
file = new StringBuilder(file.substring(startOfFileName + 1 + EOL.length()));
817851
}
818852

819853
final int line = Integer.parseInt(lineIndicator);
820-
821854
final StringBuilder msgBuffer = new StringBuilder();
822-
823855
String msg = tokens.nextToken(EOL).substring(2);
824856

825857
// Remove the 'warning: ' prefix
826-
final String warnPrefix = getWarnPrefix(msg);
858+
final String warnPrefix = getWarningPrefix(msg);
827859
if (warnPrefix != null) {
828-
isError = false;
860+
messageKind = WARNING;
829861
msg = msg.substring(warnPrefix.length());
830-
} else {
831-
isError = exitCode != 0;
832862
}
833-
834-
msgBuffer.append(msg);
835-
836-
msgBuffer.append(EOL);
863+
msgBuffer.append(msg).append(EOL);
837864

838865
String context = tokens.nextToken(EOL);
839-
840866
String pointer = null;
841867

842868
do {
843869
final String msgLine = tokens.nextToken(EOL);
844-
845870
if (pointer != null) {
846871
msgBuffer.append(msgLine);
847-
848872
msgBuffer.append(EOL);
849873
} else if (msgLine.endsWith("^")) {
850874
pointer = msgLine;
851875
} else {
852876
msgBuffer.append(context);
853-
854877
msgBuffer.append(EOL);
855-
856878
context = msgLine;
857879
}
858880
} while (tokens.hasMoreTokens());
859881

860882
msgBuffer.append(EOL);
861883

862884
final String message = msgBuffer.toString();
863-
864-
final int startcolumn = pointer.indexOf("^");
865-
885+
final int startcolumn = Objects.requireNonNull(pointer).indexOf("^");
866886
int endcolumn = (context == null) ? startcolumn : context.indexOf(" ", startcolumn);
867-
868887
if (endcolumn == -1) {
869-
endcolumn = context.length();
888+
endcolumn = Objects.requireNonNull(context).length();
870889
}
871890

872-
return new CompilerMessage(file.toString(), isError, line, startcolumn, line, endcolumn, message.trim());
891+
return new CompilerMessage(
892+
file.toString(), messageKind, line, startcolumn, line, endcolumn, message.trim());
873893
} catch (NoSuchElementException e) {
874-
return new CompilerMessage("no more tokens - could not parse error message: " + error, isError);
894+
return new CompilerMessage("no more tokens - could not parse error message: " + error, messageKind);
875895
} catch (Exception e) {
876-
return new CompilerMessage("could not parse error message: " + error, isError);
896+
return new CompilerMessage("could not parse error message: " + error, messageKind);
877897
}
878898
}
879899

880-
private static String getWarnPrefix(String msg) {
900+
private static String getWarningPrefix(String msg) {
881901
for (String warningPrefix : WARNING_PREFIXES) {
882902
if (msg.startsWith(warningPrefix)) {
883903
return warningPrefix;

0 commit comments

Comments
 (0)