| 1 | // Scintilla source code edit control |
| 2 | /** @file LexErrorList.cxx |
| 3 | ** Lexer for error lists. Used for the output pane in SciTE. |
| 4 | **/ |
| 5 | // Copyright 1998-2001 by Neil Hodgson <neilh@scintilla.org> |
| 6 | // The License.txt file describes the conditions under which this software may be distributed. |
| 7 | |
| 8 | #include <stdlib.h> |
| 9 | #include <string.h> |
| 10 | #include <stdio.h> |
| 11 | #include <stdarg.h> |
| 12 | #include <assert.h> |
| 13 | #include <ctype.h> |
| 14 | |
| 15 | #include <string> |
| 16 | #include <string_view> |
| 17 | |
| 18 | #include "ILexer.h" |
| 19 | #include "Scintilla.h" |
| 20 | #include "SciLexer.h" |
| 21 | |
| 22 | #include "WordList.h" |
| 23 | #include "LexAccessor.h" |
| 24 | #include "Accessor.h" |
| 25 | #include "StyleContext.h" |
| 26 | #include "CharacterSet.h" |
| 27 | #include "LexerModule.h" |
| 28 | |
| 29 | using namespace Lexilla; |
| 30 | |
| 31 | namespace { |
| 32 | |
| 33 | bool strstart(const char *haystack, const char *needle) noexcept { |
| 34 | return strncmp(haystack, needle, strlen(needle)) == 0; |
| 35 | } |
| 36 | |
| 37 | constexpr bool Is0To9(char ch) noexcept { |
| 38 | return (ch >= '0') && (ch <= '9'); |
| 39 | } |
| 40 | |
| 41 | constexpr bool Is1To9(char ch) noexcept { |
| 42 | return (ch >= '1') && (ch <= '9'); |
| 43 | } |
| 44 | |
| 45 | bool IsAlphabetic(int ch) { |
| 46 | return IsASCII(ch) && isalpha(ch); |
| 47 | } |
| 48 | |
| 49 | inline bool AtEOL(Accessor &styler, Sci_PositionU i) { |
| 50 | return (styler[i] == '\n') || |
| 51 | ((styler[i] == '\r') && (styler.SafeGetCharAt(i + 1) != '\n')); |
| 52 | } |
| 53 | |
| 54 | bool IsGccExcerpt(const char *s) noexcept { |
| 55 | while (*s) { |
| 56 | if (s[0] == ' ' && s[1] == '|' && (s[2] == ' ' || s[2] == '+')) { |
| 57 | return true; |
| 58 | } |
| 59 | if (!(s[0] == ' ' || s[0] == '+' || Is0To9(s[0]))) { |
| 60 | return false; |
| 61 | } |
| 62 | s++; |
| 63 | } |
| 64 | return true; |
| 65 | } |
| 66 | |
| 67 | int RecogniseErrorListLine(const char *lineBuffer, Sci_PositionU lengthLine, Sci_Position &startValue) { |
| 68 | if (lineBuffer[0] == '>') { |
| 69 | // Command or return status |
| 70 | return SCE_ERR_CMD; |
| 71 | } else if (lineBuffer[0] == '<') { |
| 72 | // Diff removal. |
| 73 | return SCE_ERR_DIFF_DELETION; |
| 74 | } else if (lineBuffer[0] == '!') { |
| 75 | return SCE_ERR_DIFF_CHANGED; |
| 76 | } else if (lineBuffer[0] == '+') { |
| 77 | if (strstart(lineBuffer, "+++ " )) { |
| 78 | return SCE_ERR_DIFF_MESSAGE; |
| 79 | } else { |
| 80 | return SCE_ERR_DIFF_ADDITION; |
| 81 | } |
| 82 | } else if (lineBuffer[0] == '-') { |
| 83 | if (strstart(lineBuffer, "--- " )) { |
| 84 | return SCE_ERR_DIFF_MESSAGE; |
| 85 | } else { |
| 86 | return SCE_ERR_DIFF_DELETION; |
| 87 | } |
| 88 | } else if (strstart(lineBuffer, "cf90-" )) { |
| 89 | // Absoft Pro Fortran 90/95 v8.2 error and/or warning message |
| 90 | return SCE_ERR_ABSF; |
| 91 | } else if (strstart(lineBuffer, "fortcom:" )) { |
| 92 | // Intel Fortran Compiler v8.0 error/warning message |
| 93 | return SCE_ERR_IFORT; |
| 94 | } else if (strstr(lineBuffer, "File \"" ) && strstr(lineBuffer, ", line " )) { |
| 95 | return SCE_ERR_PYTHON; |
| 96 | } else if (strstr(lineBuffer, " in " ) && strstr(lineBuffer, " on line " )) { |
| 97 | return SCE_ERR_PHP; |
| 98 | } else if ((strstart(lineBuffer, "Error " ) || |
| 99 | strstart(lineBuffer, "Warning " )) && |
| 100 | strstr(lineBuffer, " at (" ) && |
| 101 | strstr(lineBuffer, ") : " ) && |
| 102 | (strstr(lineBuffer, " at (" ) < strstr(lineBuffer, ") : " ))) { |
| 103 | // Intel Fortran Compiler error/warning message |
| 104 | return SCE_ERR_IFC; |
| 105 | } else if (strstart(lineBuffer, "Error " )) { |
| 106 | // Borland error message |
| 107 | return SCE_ERR_BORLAND; |
| 108 | } else if (strstart(lineBuffer, "Warning " )) { |
| 109 | // Borland warning message |
| 110 | return SCE_ERR_BORLAND; |
| 111 | } else if (strstr(lineBuffer, "at line " ) && |
| 112 | (strstr(lineBuffer, "at line " ) < (lineBuffer + lengthLine)) && |
| 113 | strstr(lineBuffer, "file " ) && |
| 114 | (strstr(lineBuffer, "file " ) < (lineBuffer + lengthLine))) { |
| 115 | // Lua 4 error message |
| 116 | return SCE_ERR_LUA; |
| 117 | } else if (strstr(lineBuffer, " at " ) && |
| 118 | (strstr(lineBuffer, " at " ) < (lineBuffer + lengthLine)) && |
| 119 | strstr(lineBuffer, " line " ) && |
| 120 | (strstr(lineBuffer, " line " ) < (lineBuffer + lengthLine)) && |
| 121 | (strstr(lineBuffer, " at " ) + 4 < (strstr(lineBuffer, " line " )))) { |
| 122 | // perl error message: |
| 123 | // <message> at <file> line <line> |
| 124 | return SCE_ERR_PERL; |
| 125 | } else if ((lengthLine >= 6) && |
| 126 | (memcmp(lineBuffer, " at " , 6) == 0) && |
| 127 | strstr(lineBuffer, ":line " )) { |
| 128 | // A .NET traceback |
| 129 | return SCE_ERR_NET; |
| 130 | } else if (strstart(lineBuffer, "Line " ) && |
| 131 | strstr(lineBuffer, ", file " )) { |
| 132 | // Essential Lahey Fortran error message |
| 133 | return SCE_ERR_ELF; |
| 134 | } else if (strstart(lineBuffer, "line " ) && |
| 135 | strstr(lineBuffer, " column " )) { |
| 136 | // HTML tidy style: line 42 column 1 |
| 137 | return SCE_ERR_TIDY; |
| 138 | } else if (strstart(lineBuffer, "\tat " ) && |
| 139 | strstr(lineBuffer, "(" ) && |
| 140 | strstr(lineBuffer, ".java:" )) { |
| 141 | // Java stack back trace |
| 142 | return SCE_ERR_JAVA_STACK; |
| 143 | } else if (strstart(lineBuffer, "In file included from " ) || |
| 144 | strstart(lineBuffer, " from " )) { |
| 145 | // GCC showing include path to following error |
| 146 | return SCE_ERR_GCC_INCLUDED_FROM; |
| 147 | } else if (strstart(lineBuffer, "NMAKE : fatal error" )) { |
| 148 | // Microsoft nmake fatal error: |
| 149 | // NMAKE : fatal error <code>: <program> : return code <return> |
| 150 | return SCE_ERR_MS; |
| 151 | } else if (strstr(lineBuffer, "warning LNK" ) || |
| 152 | strstr(lineBuffer, "error LNK" )) { |
| 153 | // Microsoft linker warning: |
| 154 | // {<object> : } (warning|error) LNK9999 |
| 155 | return SCE_ERR_MS; |
| 156 | } else if (IsGccExcerpt(lineBuffer)) { |
| 157 | // GCC code excerpt and pointer to issue |
| 158 | // 73 | GTimeVal last_popdown; |
| 159 | // | ^~~~~~~~~~~~ |
| 160 | return SCE_ERR_GCC_EXCERPT; |
| 161 | } else { |
| 162 | // Look for one of the following formats: |
| 163 | // GCC: <filename>:<line>:<message> |
| 164 | // Microsoft: <filename>(<line>) :<message> |
| 165 | // Common: <filename>(<line>): warning|error|note|remark|catastrophic|fatal |
| 166 | // Common: <filename>(<line>) warning|error|note|remark|catastrophic|fatal |
| 167 | // Microsoft: <filename>(<line>,<column>)<message> |
| 168 | // CTags: <identifier>\t<filename>\t<message> |
| 169 | // Lua 5 traceback: \t<filename>:<line>:<message> |
| 170 | // Lua 5.1: <exe>: <filename>:<line>:<message> |
| 171 | const bool initialTab = (lineBuffer[0] == '\t'); |
| 172 | bool initialColonPart = false; |
| 173 | bool canBeCtags = !initialTab; // For ctags must have an identifier with no spaces then a tab |
| 174 | enum { stInitial, |
| 175 | stGccStart, stGccDigit, stGccColumn, stGcc, |
| 176 | stMsStart, stMsDigit, stMsBracket, stMsVc, stMsDigitComma, stMsDotNet, |
| 177 | stCtagsStart, stCtagsFile, stCtagsStartString, stCtagsStringDollar, stCtags, |
| 178 | stUnrecognized |
| 179 | } state = stInitial; |
| 180 | for (Sci_PositionU i = 0; i < lengthLine; i++) { |
| 181 | const char ch = lineBuffer[i]; |
| 182 | char chNext = ' '; |
| 183 | if ((i + 1) < lengthLine) |
| 184 | chNext = lineBuffer[i + 1]; |
| 185 | if (state == stInitial) { |
| 186 | if (ch == ':') { |
| 187 | // May be GCC, or might be Lua 5 (Lua traceback same but with tab prefix) |
| 188 | if ((chNext != '\\') && (chNext != '/') && (chNext != ' ')) { |
| 189 | // This check is not completely accurate as may be on |
| 190 | // GTK+ with a file name that includes ':'. |
| 191 | state = stGccStart; |
| 192 | } else if (chNext == ' ') { // indicates a Lua 5.1 error message |
| 193 | initialColonPart = true; |
| 194 | } |
| 195 | } else if ((ch == '(') && Is1To9(chNext) && (!initialTab)) { |
| 196 | // May be Microsoft |
| 197 | // Check against '0' often removes phone numbers |
| 198 | state = stMsStart; |
| 199 | } else if ((ch == '\t') && canBeCtags) { |
| 200 | // May be CTags |
| 201 | state = stCtagsStart; |
| 202 | } else if (ch == ' ') { |
| 203 | canBeCtags = false; |
| 204 | } |
| 205 | } else if (state == stGccStart) { // <filename>: |
| 206 | state = ((ch == '-') || Is0To9(ch)) ? stGccDigit : stUnrecognized; |
| 207 | } else if (state == stGccDigit) { // <filename>:<line> |
| 208 | if (ch == ':') { |
| 209 | state = stGccColumn; // :9.*: is GCC |
| 210 | startValue = i + 1; |
| 211 | } else if (!Is0To9(ch)) { |
| 212 | state = stUnrecognized; |
| 213 | } |
| 214 | } else if (state == stGccColumn) { // <filename>:<line>:<column> |
| 215 | if (!Is0To9(ch)) { |
| 216 | state = stGcc; |
| 217 | if (ch == ':') |
| 218 | startValue = i + 1; |
| 219 | break; |
| 220 | } |
| 221 | } else if (state == stMsStart) { // <filename>( |
| 222 | state = Is0To9(ch) ? stMsDigit : stUnrecognized; |
| 223 | } else if (state == stMsDigit) { // <filename>(<line> |
| 224 | if (ch == ',') { |
| 225 | state = stMsDigitComma; |
| 226 | } else if (ch == ')') { |
| 227 | state = stMsBracket; |
| 228 | } else if ((ch != ' ') && !Is0To9(ch)) { |
| 229 | state = stUnrecognized; |
| 230 | } |
| 231 | } else if (state == stMsBracket) { // <filename>(<line>) |
| 232 | if ((ch == ' ') && (chNext == ':')) { |
| 233 | state = stMsVc; |
| 234 | } else if ((ch == ':' && chNext == ' ') || (ch == ' ')) { |
| 235 | // Possibly Delphi.. don't test against chNext as it's one of the strings below. |
| 236 | char word[512]; |
| 237 | unsigned numstep; |
| 238 | if (ch == ' ') |
| 239 | numstep = 1; // ch was ' ', handle as if it's a delphi errorline, only add 1 to i. |
| 240 | else |
| 241 | numstep = 2; // otherwise add 2. |
| 242 | Sci_PositionU chPos = 0; |
| 243 | for (Sci_PositionU j = i + numstep; j < lengthLine && IsAlphabetic(lineBuffer[j]) && chPos < sizeof(word) - 1; j++) |
| 244 | word[chPos++] = lineBuffer[j]; |
| 245 | word[chPos] = 0; |
| 246 | if (!CompareCaseInsensitive(word, "error" ) || !CompareCaseInsensitive(word, "warning" ) || |
| 247 | !CompareCaseInsensitive(word, "fatal" ) || !CompareCaseInsensitive(word, "catastrophic" ) || |
| 248 | !CompareCaseInsensitive(word, "note" ) || !CompareCaseInsensitive(word, "remark" )) { |
| 249 | state = stMsVc; |
| 250 | } else { |
| 251 | state = stUnrecognized; |
| 252 | } |
| 253 | } else { |
| 254 | state = stUnrecognized; |
| 255 | } |
| 256 | } else if (state == stMsDigitComma) { // <filename>(<line>, |
| 257 | if (ch == ')') { |
| 258 | state = stMsDotNet; |
| 259 | break; |
| 260 | } else if ((ch != ' ') && !Is0To9(ch)) { |
| 261 | state = stUnrecognized; |
| 262 | } |
| 263 | } else if (state == stCtagsStart) { |
| 264 | if (ch == '\t') { |
| 265 | state = stCtagsFile; |
| 266 | } |
| 267 | } else if (state == stCtagsFile) { |
| 268 | if ((lineBuffer[i - 1] == '\t') && |
| 269 | ((ch == '/' && chNext == '^') || Is0To9(ch))) { |
| 270 | state = stCtags; |
| 271 | break; |
| 272 | } else if ((ch == '/') && (chNext == '^')) { |
| 273 | state = stCtagsStartString; |
| 274 | } |
| 275 | } else if ((state == stCtagsStartString) && ((lineBuffer[i] == '$') && (lineBuffer[i + 1] == '/'))) { |
| 276 | state = stCtagsStringDollar; |
| 277 | break; |
| 278 | } |
| 279 | } |
| 280 | if (state == stGcc) { |
| 281 | return initialColonPart ? SCE_ERR_LUA : SCE_ERR_GCC; |
| 282 | } else if ((state == stMsVc) || (state == stMsDotNet)) { |
| 283 | return SCE_ERR_MS; |
| 284 | } else if ((state == stCtagsStringDollar) || (state == stCtags)) { |
| 285 | return SCE_ERR_CTAG; |
| 286 | } else if (initialColonPart && strstr(lineBuffer, ": warning C" )) { |
| 287 | // Microsoft warning without line number |
| 288 | // <filename>: warning C9999 |
| 289 | return SCE_ERR_MS; |
| 290 | } else { |
| 291 | return SCE_ERR_DEFAULT; |
| 292 | } |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | #define CSI "\033[" |
| 297 | |
| 298 | constexpr bool SequenceEnd(int ch) noexcept { |
| 299 | return (ch == 0) || ((ch >= '@') && (ch <= '~')); |
| 300 | } |
| 301 | |
| 302 | int StyleFromSequence(const char *seq) noexcept { |
| 303 | int bold = 0; |
| 304 | int colour = 0; |
| 305 | while (!SequenceEnd(*seq)) { |
| 306 | if (Is0To9(*seq)) { |
| 307 | int base = *seq - '0'; |
| 308 | if (Is0To9(seq[1])) { |
| 309 | base = base * 10; |
| 310 | base += seq[1] - '0'; |
| 311 | seq++; |
| 312 | } |
| 313 | if (base == 0) { |
| 314 | colour = 0; |
| 315 | bold = 0; |
| 316 | } |
| 317 | else if (base == 1) { |
| 318 | bold = 1; |
| 319 | } |
| 320 | else if (base >= 30 && base <= 37) { |
| 321 | colour = base - 30; |
| 322 | } |
| 323 | } |
| 324 | seq++; |
| 325 | } |
| 326 | return SCE_ERR_ES_BLACK + bold * 8 + colour; |
| 327 | } |
| 328 | |
| 329 | void ColouriseErrorListLine( |
| 330 | const std::string &lineBuffer, |
| 331 | Sci_PositionU endPos, |
| 332 | Accessor &styler, |
| 333 | bool valueSeparate, |
| 334 | bool escapeSequences) { |
| 335 | Sci_Position startValue = -1; |
| 336 | const Sci_PositionU lengthLine = lineBuffer.length(); |
| 337 | const int style = RecogniseErrorListLine(lineBuffer.c_str(), lengthLine, startValue); |
| 338 | if (escapeSequences && strstr(lineBuffer.c_str(), CSI)) { |
| 339 | const Sci_Position startPos = endPos - lengthLine; |
| 340 | const char *linePortion = lineBuffer.c_str(); |
| 341 | Sci_Position startPortion = startPos; |
| 342 | int portionStyle = style; |
| 343 | while (const char *startSeq = strstr(linePortion, CSI)) { |
| 344 | if (startSeq > linePortion) { |
| 345 | styler.ColourTo(startPortion + static_cast<int>(startSeq - linePortion), portionStyle); |
| 346 | } |
| 347 | const char *endSeq = startSeq + 2; |
| 348 | while (!SequenceEnd(*endSeq)) |
| 349 | endSeq++; |
| 350 | const Sci_Position endSeqPosition = startPortion + static_cast<Sci_Position>(endSeq - linePortion) + 1; |
| 351 | switch (*endSeq) { |
| 352 | case 0: |
| 353 | styler.ColourTo(endPos, SCE_ERR_ESCSEQ_UNKNOWN); |
| 354 | return; |
| 355 | case 'm': // Colour command |
| 356 | styler.ColourTo(endSeqPosition, SCE_ERR_ESCSEQ); |
| 357 | portionStyle = StyleFromSequence(startSeq+2); |
| 358 | break; |
| 359 | case 'K': // Erase to end of line -> ignore |
| 360 | styler.ColourTo(endSeqPosition, SCE_ERR_ESCSEQ); |
| 361 | break; |
| 362 | default: |
| 363 | styler.ColourTo(endSeqPosition, SCE_ERR_ESCSEQ_UNKNOWN); |
| 364 | portionStyle = style; |
| 365 | } |
| 366 | startPortion = endSeqPosition; |
| 367 | linePortion = endSeq + 1; |
| 368 | } |
| 369 | styler.ColourTo(endPos, portionStyle); |
| 370 | } else { |
| 371 | if (valueSeparate && (startValue >= 0)) { |
| 372 | styler.ColourTo(endPos - (lengthLine - startValue), style); |
| 373 | styler.ColourTo(endPos, SCE_ERR_VALUE); |
| 374 | } else { |
| 375 | styler.ColourTo(endPos, style); |
| 376 | } |
| 377 | } |
| 378 | } |
| 379 | |
| 380 | void ColouriseErrorListDoc(Sci_PositionU startPos, Sci_Position length, int, WordList *[], Accessor &styler) { |
| 381 | std::string lineBuffer; |
| 382 | styler.StartAt(startPos); |
| 383 | styler.StartSegment(startPos); |
| 384 | |
| 385 | // property lexer.errorlist.value.separate |
| 386 | // For lines in the output pane that are matches from Find in Files or GCC-style |
| 387 | // diagnostics, style the path and line number separately from the rest of the |
| 388 | // line with style 21 used for the rest of the line. |
| 389 | // This allows matched text to be more easily distinguished from its location. |
| 390 | const bool valueSeparate = styler.GetPropertyInt("lexer.errorlist.value.separate" , 0) != 0; |
| 391 | |
| 392 | // property lexer.errorlist.escape.sequences |
| 393 | // Set to 1 to interpret escape sequences. |
| 394 | const bool escapeSequences = styler.GetPropertyInt("lexer.errorlist.escape.sequences" ) != 0; |
| 395 | |
| 396 | for (Sci_PositionU i = startPos; i < startPos + length; i++) { |
| 397 | lineBuffer.push_back(styler[i]); |
| 398 | if (AtEOL(styler, i)) { |
| 399 | // End of line met, colourise it |
| 400 | ColouriseErrorListLine(lineBuffer, i, styler, valueSeparate, escapeSequences); |
| 401 | lineBuffer.clear(); |
| 402 | } |
| 403 | } |
| 404 | if (!lineBuffer.empty()) { // Last line does not have ending characters |
| 405 | ColouriseErrorListLine(lineBuffer, startPos + length - 1, styler, valueSeparate, escapeSequences); |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | const char *const emptyWordListDesc[] = { |
| 410 | nullptr |
| 411 | }; |
| 412 | |
| 413 | } |
| 414 | |
| 415 | LexerModule lmErrorList(SCLEX_ERRORLIST, ColouriseErrorListDoc, "errorlist" , 0, emptyWordListDesc); |
| 416 | |