[Microsoft.Build.Utilities] Fixed MSBuildErrorParser to be more robust
[mono.git] / mcs / class / Microsoft.Build.Utilities / Microsoft.Build.Utilities / MSBuildErrorParser.cs
1 //
2 // MSBuildErrorParser.cs: Parser for MSBuild-format error messages.
3 //
4 // Author:
5 //   Michael Hutchinson (m.j.hutchinson@gmail.com)
6 //
7 // Copyright 2014 Xamarin Inc. (http://www.xamarin.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 //
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 //
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
28 using System;
29
30 namespace Microsoft.Build.Utilities
31 {
32         static class MSBuildErrorParser
33         {
34                 public class Result
35                 {
36                         public string Origin { get; set; }
37                         public int Line { get; set; }
38                         public int Column { get; set; }
39                         public int EndLine { get; set; }
40                         public int EndColumn { get; set; }
41                         public string Subcategory { get; set; }
42                         public bool IsError { get; set; }
43                         public string Code { get; set; }
44                         public string Message { get; set; }
45                 }
46
47                 // Parses single-line error message in the standard MSBuild error format:
48                 //
49                 // [origin[(position)]:][subcategory] category code: [message]
50                 //
51                 // Components in [] square brackets are optional.
52                 // Components are as follows:
53                 //  origin: tool name or filename, may contain whitespace, no colons except the drive letter
54                 //  position: line/col position or range in the file, with one of the following forms:
55                 //      (l), (l,c), (l,c-c), (l,c,l,c)
56                 //  subcategory: arbitrary text, may contain whitepsace
57                 //  code: error code, no whietspace or punctuation
58                 //  message: arbitraty text, no restrictions
59                 //
60                 public static Result TryParseLine (string line)
61                 {
62                         int originEnd, originStart = 0;
63                         var result = new Result ();
64
65                         MoveNextNonSpace (line, ref originStart);
66
67                         if (originStart >= line.Length)
68                                 return null;
69
70                         //find the origin section
71                         //the filename may include a colon for Windows drive e.g. C:\foo, so ignore colon in first 2 chars
72                         if (line[originStart] != ':') {
73                                 if (originStart + 2 >= line.Length)
74                                         return null;
75
76                                 if ((originEnd = line.IndexOf (':', originStart + 2) - 1) < 0)
77                                         return null;
78                         } else {
79                                 originEnd = originStart;
80                         }
81
82                         int categoryStart = originEnd + 2;
83
84                         if (categoryStart >= line.Length)
85                                 return null;
86
87                         MovePrevNonSpace (line, ref originEnd);
88
89                         //if there is no origin section, then we can't parse the message
90                         if (originEnd < 0 || originEnd < originStart)
91                                 return null;
92
93                         //find the category section, if there is one
94                         MoveNextNonSpace (line, ref categoryStart);
95
96                         if (categoryStart >= line.Length)
97                                 return null;
98
99                         int categoryEnd = line.IndexOf (':', categoryStart) - 1;
100                         int messageStart = categoryEnd + 2;
101
102                         if (categoryEnd >= 0) {
103                                 MovePrevNonSpace (line, ref categoryEnd);
104                                 if (categoryEnd <= categoryStart)
105                                         categoryEnd = -1;
106                         }
107
108                         //if there is a category section and it parses
109                         if (categoryEnd > 0 && ParseCategory (line, categoryStart, categoryEnd, result)) {
110                                 //then parse the origin section
111                                 if (originEnd > originStart && !ParseOrigin (line, originStart, originEnd, result))
112                                         return null;
113                         } else {
114                                 //there is no origin, parse the origin section as if it were the category
115                                 if (!ParseCategory (line, originStart, originEnd, result))
116                                         return null;
117                                 messageStart = categoryStart;
118                         }
119
120                         //read the remaining message
121                         MoveNextNonSpace (line, ref messageStart);
122                         int messageEnd = line.Length - 1;
123                         MovePrevNonSpace (line, ref messageEnd, messageStart);
124                         if (messageEnd > messageStart) {
125                                 result.Message = line.Substring (messageStart, messageEnd - messageStart + 1);
126                         } else {
127                                 result.Message = "";
128                         }
129
130                         return result;
131                 }
132
133                 // filename (line,col) | tool :
134                 static bool ParseOrigin (string line, int start, int end, Result result)
135                 {
136                         // no line/col
137                         if (line [end] != ')') {
138                                 result.Origin = line.Substring (start, end - start + 1);
139                                 return true;
140                         }
141
142                         //scan back for matching (, assuming at least one char between them
143                         int posStart = line.LastIndexOf ('(', end - 2, end - start - 2);
144                         if (posStart < 0)
145                                 return false;
146
147                         if (!ParsePosition (line, posStart + 1, end, result)) {
148                                 result.Origin = line.Substring (start, end - start + 1);
149                                 return true;
150                         }
151
152                         end = posStart - 1;
153                         MovePrevNonSpace (line, ref end, start);
154
155                         result.Origin = line.Substring (start, end - start + 1);
156                         return true;
157                 }
158
159                 static bool ParseLineColVal (string str, out int val)
160                 {
161                         try {
162                                 val = int.Parse (str);
163                                 return true;
164                         } catch (OverflowException) {
165                                 val = 0;
166                                 return true;
167                         } catch (FormatException) {
168                                 val = 0;
169                                 return false;
170                         }
171                 }
172
173                 // Supported combos:
174                 //
175                 // (SL,SC,EL,EC)
176                 // (SL,SC-EC)
177                 // (SL-EL)
178                 // (SL,SC)
179                 // (SL)
180                 //
181                 // Unexpected patterns of commas/dashes abort parsing, discarding all values.
182                 // Any other characters abort parsing and the (...) gets treated as pert of the filename.
183                 // Overflows are silently treated as zeroes.
184                 //
185                 static bool ParsePosition (string str, int start, int end, Result result)
186                 {
187                         int line = 0, col = 0, endLine = 0, endCol = 0;
188
189                         var a = str.Substring (start, end - start).Split (',');
190
191                         if (a.Length > 4 || a.Length == 3)
192                                 return true;
193
194                         if (a.Length == 4) {
195                                 bool valid =
196                                         ParseLineColVal (a [0], out line) &&
197                                         ParseLineColVal (a [1], out col) &&
198                                         ParseLineColVal (a [2], out endLine) &&
199                                         ParseLineColVal (a [3], out endCol);
200                                 if (!valid)
201                                         return false;
202                         } else {
203                                 var b = a [0].Split ('-');
204                                 if (b.Length > 2)
205                                         return true;
206                                 if (!ParseLineColVal (b [0], out line))
207                                         return false;
208                                 if (b.Length == 2) {
209                                         if (a.Length == 2)
210                                                 return true;
211                                         if (!ParseLineColVal (b [1], out endLine))
212                                                 return false;
213                                 }
214                                 if (a.Length == 2) {
215                                         var c = a [1].Split ('-');
216                                         if (c.Length > 2)
217                                                 return true;
218                                         if (!ParseLineColVal (c [0], out col))
219                                                 return false;
220                                         if (c.Length == 2) {
221                                                 if (!ParseLineColVal (c [1], out endCol))
222                                                         return false;
223                                         }
224                                 }
225                         }
226
227                         result.Line = line;
228                         result.Column = col;
229                         result.EndLine = endLine;
230                         result.EndColumn = endCol;
231                         return true;
232                 }
233
234                 static bool ParseCategory (string line, int start, int end, Result result)
235                 {
236                         int idx = end;
237                         MovePrevWordStart (line, ref idx, start);
238                         if (idx < start + 1)
239                                 return false;
240
241                         string code = line.Substring (idx, end - idx + 1);
242
243                         idx--;
244                         MovePrevNonSpace (line, ref idx, start);
245                         end = idx;
246                         MovePrevWordStart (line, ref idx, start);
247                         if (idx < start)
248                                 return false;
249
250                         string category = line.Substring (idx , end - idx + 1);
251                         if (string.Equals (category, "error", StringComparison.OrdinalIgnoreCase))
252                                 result.IsError = true;
253                         else if (!string.Equals (category, "warning", StringComparison.OrdinalIgnoreCase))
254                                 return false;
255
256                         result.Code = code;
257
258                         idx--;
259                         if (idx > start) {
260                                 MovePrevNonSpace (line, ref idx, start);
261                                 result.Subcategory = line.Substring (start, idx - start + 1);
262                         } else {
263                                 result.Subcategory = "";
264                         }
265
266                         return true;
267                 }
268
269                 static void MoveNextNonSpace (string s, ref int idx)
270                 {
271                         while (idx < s.Length && char.IsWhiteSpace (s[idx]))
272                                 idx++;
273                 }
274
275                 static void MovePrevNonSpace (string s, ref int idx, int min = 0)
276                 {
277                         while (idx > min && char.IsWhiteSpace (s[idx]))
278                                 idx--;
279                 }
280
281                 static void MovePrevWordStart (string s, ref int idx, int min = 0)
282                 {
283                         while (idx > min && char.IsLetterOrDigit (s[idx - 1]))
284                                 idx--;
285                 }
286         }
287 }