62
62
import java .util .List ;
63
63
import java .util .Map ;
64
64
import java .util .NoSuchElementException ;
65
+ import java .util .Objects ;
65
66
import java .util .Properties ;
66
67
import java .util .StringTokenizer ;
67
68
import java .util .concurrent .ConcurrentLinkedDeque ;
80
81
import org .codehaus .plexus .util .cli .CommandLineUtils ;
81
82
import org .codehaus .plexus .util .cli .Commandline ;
82
83
84
+ import static org .codehaus .plexus .compiler .CompilerMessage .Kind .*;
85
+ import static org .codehaus .plexus .compiler .javac .JavacCompiler .Messages .*;
86
+
83
87
/**
84
88
* @author <a href="mailto:[email protected] ">Trygve Laugstøl</a>
85
89
* @author <a href="mailto:[email protected] ">Matthew Pocock</a>
86
90
* @author <a href="mailto:[email protected] ">Jörg Waßmer</a>
91
+ * @author Alexander Kriegisch
87
92
* @author Others
88
93
*
89
94
*/
90
95
@ Named ("javac" )
91
96
@ Singleton
92
97
public class JavacCompiler extends AbstractCompiler {
93
98
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: " };
96
123
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: " };
99
126
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
+ }
102
133
103
134
private static final Object LOCK = new Object ();
104
135
@@ -563,6 +594,11 @@ protected CompilerResult compileOutOfProcess(CompilerConfiguration config, Strin
563
594
}
564
595
565
596
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.
566
602
returnCode = CommandLineUtils .executeCommandLine (cli , out , out );
567
603
568
604
messages = parseModernStream (returnCode , new BufferedReader (new StringReader (out .getOutput ())));
@@ -643,69 +679,21 @@ private static CompilerResult compileInProcess0(Class<?> javacClass, String[] ar
643
679
Pattern .compile ("^(?:javac:|Error occurred during initialization of (?:boot layer|VM)).*" , Pattern .DOTALL );
644
680
645
681
/**
646
- * Parse the output from the compiler into a list of CompilerMessage objects
682
+ * Parse the compiler output into a list of compiler messages
647
683
*
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
652
688
*/
653
689
static List <CompilerMessage > parseModernStream (int exitCode , BufferedReader input ) throws IOException {
654
690
List <CompilerMessage > errors = new ArrayList <>();
655
-
656
691
String line ;
657
-
658
692
StringBuilder buffer = new StringBuilder ();
659
-
660
693
boolean hasPointer = false ;
661
694
int stackTraceLineCount = 0 ;
662
695
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 ) {
709
697
if (stackTraceLineCount == 0 && STACK_TRACE_FIRST_LINE .matcher (line ).matches ()
710
698
|| STACK_TRACE_OTHER_LINE .matcher (line ).matches ()) {
711
699
stackTraceLineCount ++;
@@ -717,33 +705,77 @@ static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader inpu
717
705
if (!line .startsWith (" " ) && hasPointer ) {
718
706
// add the error bean
719
707
errors .add (parseModernError (exitCode , buffer .toString ()));
720
-
721
708
// reset for next error block
722
709
buffer = new StringBuilder (); // this is quicker than clearing it
723
-
724
710
hasPointer = false ;
725
711
}
726
712
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
+ }
737
729
} else {
738
- buffer .append (line );
739
-
740
- buffer .append (EOL );
730
+ // add next unclassified line to buffer
731
+ buffer .append (line ).append (EOL );
741
732
}
742
733
743
734
if (line .endsWith ("^" )) {
744
735
hasPointer = true ;
745
736
}
746
737
}
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 ;
747
779
}
748
780
749
781
private static boolean isMisc (String line ) {
@@ -754,6 +786,14 @@ private static boolean isNote(String line) {
754
786
return startsWithPrefix (line , NOTE_PREFIXES );
755
787
}
756
788
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
+
757
797
private static boolean startsWithPrefix (String line , String [] prefixes ) {
758
798
for (String prefix : prefixes ) {
759
799
if (line .startsWith (prefix )) {
@@ -764,26 +804,24 @@ private static boolean startsWithPrefix(String line, String[] prefixes) {
764
804
}
765
805
766
806
/**
767
- * Construct a CompilerMessage object from a line of the compiler output
807
+ * Construct a compiler message object from a compiler output line
768
808
*
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
772
812
*/
773
813
static CompilerMessage parseModernError (int exitCode , String error ) {
774
814
final StringTokenizer tokens = new StringTokenizer (error , ":" );
775
815
776
- boolean isError = exitCode != 0 ;
816
+ CompilerMessage . Kind messageKind = exitCode == 0 ? WARNING : ERROR ;
777
817
778
818
try {
779
819
// 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.
782
822
783
823
boolean tokenIsAnInteger ;
784
-
785
824
StringBuilder file = null ;
786
-
787
825
String currentToken = null ;
788
826
789
827
do {
@@ -796,9 +834,7 @@ static CompilerMessage parseModernError(int exitCode, String error) {
796
834
}
797
835
798
836
currentToken = tokens .nextToken ();
799
-
800
837
// Probably the only backward compatible means of checking if a string is an integer.
801
-
802
838
tokenIsAnInteger = true ;
803
839
804
840
try {
@@ -809,75 +845,59 @@ static CompilerMessage parseModernError(int exitCode, String error) {
809
845
} while (!tokenIsAnInteger );
810
846
811
847
final String lineIndicator = currentToken ;
812
-
813
- final int startOfFileName = file .toString ().lastIndexOf (']' );
814
-
848
+ final int startOfFileName = Objects .requireNonNull (file ).toString ().lastIndexOf (']' );
815
849
if (startOfFileName > -1 ) {
816
850
file = new StringBuilder (file .substring (startOfFileName + 1 + EOL .length ()));
817
851
}
818
852
819
853
final int line = Integer .parseInt (lineIndicator );
820
-
821
854
final StringBuilder msgBuffer = new StringBuilder ();
822
-
823
855
String msg = tokens .nextToken (EOL ).substring (2 );
824
856
825
857
// Remove the 'warning: ' prefix
826
- final String warnPrefix = getWarnPrefix (msg );
858
+ final String warnPrefix = getWarningPrefix (msg );
827
859
if (warnPrefix != null ) {
828
- isError = false ;
860
+ messageKind = WARNING ;
829
861
msg = msg .substring (warnPrefix .length ());
830
- } else {
831
- isError = exitCode != 0 ;
832
862
}
833
-
834
- msgBuffer .append (msg );
835
-
836
- msgBuffer .append (EOL );
863
+ msgBuffer .append (msg ).append (EOL );
837
864
838
865
String context = tokens .nextToken (EOL );
839
-
840
866
String pointer = null ;
841
867
842
868
do {
843
869
final String msgLine = tokens .nextToken (EOL );
844
-
845
870
if (pointer != null ) {
846
871
msgBuffer .append (msgLine );
847
-
848
872
msgBuffer .append (EOL );
849
873
} else if (msgLine .endsWith ("^" )) {
850
874
pointer = msgLine ;
851
875
} else {
852
876
msgBuffer .append (context );
853
-
854
877
msgBuffer .append (EOL );
855
-
856
878
context = msgLine ;
857
879
}
858
880
} while (tokens .hasMoreTokens ());
859
881
860
882
msgBuffer .append (EOL );
861
883
862
884
final String message = msgBuffer .toString ();
863
-
864
- final int startcolumn = pointer .indexOf ("^" );
865
-
885
+ final int startcolumn = Objects .requireNonNull (pointer ).indexOf ("^" );
866
886
int endcolumn = (context == null ) ? startcolumn : context .indexOf (" " , startcolumn );
867
-
868
887
if (endcolumn == -1 ) {
869
- endcolumn = context .length ();
888
+ endcolumn = Objects . requireNonNull ( context ) .length ();
870
889
}
871
890
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 ());
873
893
} 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 );
875
895
} 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 );
877
897
}
878
898
}
879
899
880
- private static String getWarnPrefix (String msg ) {
900
+ private static String getWarningPrefix (String msg ) {
881
901
for (String warningPrefix : WARNING_PREFIXES ) {
882
902
if (msg .startsWith (warningPrefix )) {
883
903
return warningPrefix ;
0 commit comments