diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /devtools/parse_analyze_errors.py | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'devtools/parse_analyze_errors.py')
| -rw-r--r-- | devtools/parse_analyze_errors.py | 574 |
1 files changed, 574 insertions, 0 deletions
diff --git a/devtools/parse_analyze_errors.py b/devtools/parse_analyze_errors.py new file mode 100644 index 0000000..e70ea80 --- /dev/null +++ b/devtools/parse_analyze_errors.py @@ -0,0 +1,574 @@ +# This script is used to parse the results of the Visual C++ /analyze feature. +# See the 'usage' section for details. + +# Regular expression experimentation was done at http://www.pythonregex.com/ + +# The buildbot warning parser that looks at this script uses the default compile warning +# parser which is documented at http://buildbot.net/buildbot/docs/0.8.4/Compile.html +# The regex used is '.*warning[: ].*'. This means that any instance of 'warning:' or +# 'warning ' will be flagged as a warning. The check is case sensitive so Warning will +# not be flagged as a warning. This script remaps warning to 'wrning' in some places so +# that lists of fixed warnings or old warnings will not trigger warning detection. +# Similarly it remaps error to 'eror'. + +# Typical warning messages might look like this: +# 2>d:\dota\src\tier1\bitbuf.cpp(1336): warning C6001: Using uninitialized memory 'retval': Lines: 1327, 1328, 1331, 1332, 1333, 1334, 1336 + +import re +import sys +import os + +# Grab per-project configuration information from the analyzeconfig package +import analyzeconfig +ignorePaths = analyzeconfig.ignorePaths +alwaysFatalWarnings = analyzeconfig.alwaysFatalWarnings.keys() +fatalWhenNewWarnings = analyzeconfig.fatalWhenNewWarnings.keys() +remaps = analyzeconfig.remaps +informationalWarnings = analyzeconfig.informationalWarnings + +lkgFilename = "analyzelkg.txt" + +# This matches 0-3 digits and an optional '>' character. Some builds prefix the output +# with '10>' or something equivalent, but some builds do not. +prefixRePattern = r"\d?\d?\d?>?" + +warningWithLinesRe = re.compile(prefixRePattern + r"(.*)\((\d+)\): warning C(\d{4,5})(.*)(: Lines:.*)") +warningRe = re.compile(prefixRePattern + r"(.*)\((\d+)\): warning C(\d{4,5})(.*)") +errorRe = re.compile(prefixRePattern + r"(.*)\((\d+)\): error C(\d{4,5})(.*)") + +# For reparsing the keys that we use to store the parsed log data: +# The format for keys is like this: +# key = "%s %s in %s" % (type, warningNumber, filename) +parseKeyRe = re.compile(r"(.*) (\d{4,5}) in (.*)") + +warningsToText = { + 2719 : "Formal parameter with __declspec(align('n')) won't be aligned", + 4005 : "Macro redefinition", + 4100 : "Unreferenced formal parameter", + 4189 : "Local variable is initialized but not referenced", + 4245 : "Signed/unsigned mismatch", + 4505 : "Unreferenced local function has been removed", + 4611 : "interaction between '_setjmp' and C++ object destruction is non-portable", + 4703 : "Potentially uninitialized local pointer variable used", + 4789 : "Destination of memory copy is too small", + 6001 : "Using uninitialized memory", + 6029 : "Possible buffer overrun: use of unchecked value", + 6053 : "Call to <function> may not zero-terminate string", + 6054 : "String may not be zero-terminated", + 6057 : "Buffer overrun due to number of characters/number of bytes mismatch", + 6059 : "Incorrect length parameter", + 6063 : "Missing string argument", + 6064 : "Missing integer argument", + 6066 : "Non-pointer passed as parameter when pointer is required", + 6067 : "Parameter in call must be the address of the string", + 6200 : "Index is out of valid index range for non-stack buffer", + 6201 : "Out of range index", + 6202 : "Buffer overrun for stack allocated variable in call to function", + 6203 : "Buffer overrun for non-stack buffer", + 6204 : "Possible buffer overrun: use of unchecked parameter", + 6209 : "Using sizeof when a character count might be needed. Annotate with OUT_Z_CAP or its relatives", + 6216 : "Compiler-inserted cast between semantically different integral types: a Boolean type to HRESULT", + 6221 : "Implicit cast between semantically different integer types", + 6219 : "Implicit cast between semantically different integer types", + 6236 : "(<expression> || <non-zero constant>) is always a non-zero constant", + 6244 : "Local declaration shadows declaration of same name in global scope", + 6246 : "Local declaration shadows declaration of same name in outer scope", + 6248 : "Setting a SECURITY_DESCRIPTOR's DACL to NULL will result in an unprotected object", + 6258 : "Using TerminateThread does not allow proper thread clean up", + 6262 : "Excessive stack usage in function", + 6263 : "Using _alloca in a loop: this can quickly overflow stack", + 6269 : "Possible incorrect order of operations: dereference ignored", + 6270 : "Missing float argument to varargs function", + 6271 : "Extra argument passed: parameter is not used by the format string", + 6272 : "Non-float passed as argument <number> when float is required", + 6273 : "Non-integer passed as a parameter when integer is required", + 6277 : "NULL application name with an unquoted path results in a security vulnerability if the path contains spaces", + 6278 : "Buffer is allocated with array new [], but deleted with scalar delete. Destructors will not be called", + 6281 : "Incorrect order of operations: relational operators have higher precedence than bitwise operators", + 6282 : "Incorrect operator: assignment of constant in Boolean context", + 6283 : "Buffer is allocated with array new [], but deleted with scalar delete", + 6284 : "Object passed as a parameter when string is required", + 6286 : "(<non-zero constant> || <expression>) is always a non-zero constant.", + 6287 : "Redundant code: the left and right sub-expressions are identical", + 6290 : "Bitwise operation on logical result: ! has higher precedence than &. Use && or (!(x & y)) instead", + 6293 : "Ill-defined for-loop: counts down from minimum", + 6294 : "Ill-defined for-loop: initial condition does not satisfy test. Loop body not executed", + 6295 : "Ill-defined for-loop: Loop executed indefinitely", + 6297 : "Arithmetic overflow: 32-bit value is shifted, then cast to 64-bit value", + 6298 : "Using a read-only string <pointer> as a writable string argument", + 6302 : "Format string mismatch: character string passed as parameter when wide character string is required", + 6306 : "Incorrect call to 'fprintf*': consider using 'vfprintf*' which accepts a va_list as an argument", + 6313 : "Incorrect operator: zero-valued flag cannot be tested with bitwise-and. Use an equality test to check for zero-valued flags", + 6316 : "Incorrect operator: tested expression is constant and non-zero. Use bitwise-and to determine whether bits are set", + 6318 : "Ill-defined __try/__except: use of the constant EXCEPTION_CONTINUE_SEARCH ", + 6328 : "Wrong parameter type passed", + 6330 : "'const char' passed as a parameter when 'unsigned char' is required", + 6333 : "Invalid parameter: passing MEM_RELEASE and a non-zero dwSize parameter to 'VirtualFree' is not allowed", + 6334 : "Sizeof operator applied to an expression with an operator might yield unexpected results", + 6336 : "Arithmetic operator has precedence over question operator, use parentheses to clarify intent", + 6385 : "Out of range read", + 6386 : "Out of range write", + 6522 : "Invalid size specification: expression must be of integral type", + 6523 : "Invalid size specification: parameter 'size' not found", + 28199 : "Using possibly uninitialized: The variable has had its address taken but no assignment to it has been discovered.", + } + + + +def Cleanup(textline): + for sourcePath in remaps.keys(): + if textline.startswith(sourcePath): + return textline.replace(sourcePath, remaps[sourcePath]) + return textline + + + +def ParseLog(logName): + # Create a dictionary in which to store the results + # The keys for the dictionary are "warning 6328 in c:\buildbot\..." + # This means that the count of keys is not particularly meaningful. The + # length of each data item tells you the total number of raw warnings, but + # some of those are duplicates (from the same file being compiled multiple + # times). The UniqueWarningCount function can be used to find the number of + # unique warnings in each record. + # + # This probably could have been designed better, perhaps by having the key + # include the line number. Probably not worth changing now. + result = {} + lines = open(logName).readlines() + + # First look for compiler crashes. Joy. + if analyzeconfig.abortOnCompilerCrash: + compilerCrashes = 0 + for line in lines: + # Look for signs that the compiler crashed and if it did then abort. + if line.count("Please choose the Technical Support command on the Visual C++") > 0: + compilerCrashes += 1 + # Print a message in the warning format so that we can see how many times the + # compiler crashed on the buildbot waterfall page. + print "cl.exe(1): warning : internal compiler error, the compiler has crashed. Aborting code analysis." + # If the compiler crashes one or more times then give up. + if compilerCrashes > 0: + sys.exit(0) + + warningCount = 0 + ignoredCount = 0 + namePrinted = False + for line in lines: + # Some of the paths in the output lines have slashes instead of backslashes. + line = line.replace("/", "\\") + ignored = False + for path in ignorePaths: + if line.count(path) > 0: + ignored = True + ignoredCount += 1 + if ignored: + continue + filename = "" + type = "warning" + # Look for warnings with filename and line number. The groups returned + # are: + # file name + # line number + # warning number + # warning text + # optionally (warningWithLinesRe only) the lines implicated in the warning + warningMatch = warningWithLinesRe.match(line) + if not warningMatch: + warningMatch = warningRe.match(line) + if not warningMatch: + warningMatch = errorRe.match(line) + if warningMatch: + type = "error" + + # We want to record how many errors of a particular type occur in a particular source + # file so we create a dictionary with [file name, warning number, isError] as the key. + if warningMatch: + filename = warningMatch.groups()[0] + lineNumber = warningMatch.groups()[1] + warningNumber = warningMatch.groups()[2] + warningText = warningMatch.groups()[3] + key = "%s %s in %s" % (type, warningNumber, filename) + data = "%s(%s): %s C%s%s" % (filename, lineNumber, type, warningNumber, warningText) + warningCount += 1 + if key in result: + result[key] += [data] + else: + result[key] = [data] + elif line.find(": warning") >= 0: + pass # Ignore these warnings for now + elif line.find(": error ") >= 0: + if not namePrinted: + namePrinted = True + print " Unhandled errors found in '%s'" % logName + print " %s" % line.strip() + + uniqueWarningCount = 0 + uniqueInformationalCount = 0 + for key in result.keys(): + count = UniqueWarningCount(result[key]) + match = parseKeyRe.match(key) + warningNumber = match.groups()[1] + if warningNumber in informationalWarnings: + uniqueInformationalCount += count + else: + uniqueWarningCount += count + + print "%d lines of output in %s, %d issues found, %d ignored, plus %d informational." % (len(lines), logName, uniqueWarningCount, ignoredCount, uniqueInformationalCount) + print "" + return result + + + +# The output of this script is filtered by buildbot as described at +# http://buildbot.net/buildbot/docs/0.8.4/Compile.html which means that the +# warning text is generated by running it through re.match(".*warning[: ].*") +# The e-mails are generated by running them through BuildAnalyze.createSummary +# in //steam/main/tools/buildbot/shared_helpers.py. The two sets of regexes +# should be kept compatible. +# The matching is case sensitive so Warning is not matched. + +def PrintEntries(newEntries, prefix, sanitize): + printedAlready = {} + for newEntry in newEntries: + if not newEntry in printedAlready: + printedAlready[newEntry] = True + # When printing out the list of warnings that have been fixed + # replace ": warning" with a string that will not be + # recognized by the buildbot parser as a warning so that the + # break e-mails will only include new warnings. + # Yes, this is a hack. In the future a custom parser/filter + # for the e-mails would be better. + if sanitize: + newEntry = newEntry.replace(": warning", ": wrning") + newEntry = newEntry.replace(": error", ": eror") + print "%s%s" % (prefix, Cleanup(newEntry)) + + + +def UniqueWarningCount(warningRecord): + # Warnings may be encountered multiple times (header files included + # from many places, or source files compiled multiple times) and these + # are all added to the warning record. However, for determining + # unique warnings we want to filter out these duplicates. + alreadySeen = {} + count = 0 + for warning in warningRecord: + if not warning in alreadySeen: + alreadySeen[warning] = True + count += 1 + return count + + + +def DumpNewWarnings(old, new, oldname, newname): + newWarningsFound = False + warningsFixed = False + fatalWarningsFound = False + + warningCounts = {} + oldWarningCounts = {} + sampleWarnings = {} + + for key in new.keys(): + match = parseKeyRe.match(key) + warningNumber = int(match.groups()[1]) + if warningNumber in alwaysFatalWarnings: + fatalWarningsFound = True + if warningNumber in warningCounts: + warningCounts[warningNumber] += UniqueWarningCount(new[key]) + else: + warningCounts[warningNumber] = UniqueWarningCount(new[key]) + sampleWarnings[warningNumber] = new[key][0] + if not key in old: + newWarningsFound = True + if warningNumber in fatalWhenNewWarnings: + fatalWarningsFound = True + for key in old.keys(): + match = parseKeyRe.match(key) + warningNumber = int(match.groups()[1]) + if warningNumber in oldWarningCounts: + oldWarningCounts[warningNumber] += UniqueWarningCount(old[key]) + else: + oldWarningCounts[warningNumber] = UniqueWarningCount(old[key]) + if not warningNumber in sampleWarnings: + sampleWarnings[warningNumber] = old[key][0] + if not key in new: + warningsFixed = True + + if fatalWarningsFound: + errorCode = 10 + elif newWarningsFound: + errorCode = 10 + else: + errorCode = 0 + + # Make three passes through the warnings so that we group fatal, fatal-when-new, and + # new warnings together, with the fatal warnings first. + # The colons at the beginning of blank lines are so that buildbot's BuildAnalyze.createSummary + # will retain those lines. + for type in ["Fatal", "Fatal-when-new", "New"]: + fixing = "required" + if type == "New": + fixing = "optional" + message = "%s warning or warnings found. Fixing these is %s:\n:" % (type, fixing) + for key in new.keys(): + newEntries = new[key] + match = parseKeyRe.match(key) + warningNumber = int(match.groups()[1]) + if warningNumber in alwaysFatalWarnings: + if type == "Fatal": + print message + message = ":" + PrintEntries(newEntries, " ", False) + elif not key in old: + if warningNumber in fatalWhenNewWarnings: + if type == "Fatal-when-new": + print message + message = ":" + PrintEntries(newEntries, " ", False) + else: + if type == "New": + print message + message = ":" + PrintEntries(newEntries, " ", False) + + # If message is short then that means it was printed and then assigned to a short + # string, which means some warnings of this type were printed, which means we should + # print a separator. + if len(message) < 2: + print ":\n:\n:\n:\n:" + + + + if warningsFixed: + print "\n\n\n\n\nOld issues that have been fixed:" + for key in old.keys(): + oldEntries = old[key] + if not key in new: + print "Warning fixed in %s:" % newname + print "%d times:" % len(oldEntries) + PrintEntries(oldEntries, " ", True) + print "" + else: + newEntries = new[key] + # Disable printing decreased warning counts -- too much noise. + if False and len(newEntries) < len(oldEntries): + print "Decreased wrning count:" + print " Old (%s):" % oldname + print " %d times:" % len(oldEntries) + PrintEntries(oldEntries, " ", True) + print " New (%s):" % newname + print " %d times:" % len(newEntries) + PrintEntries(newEntries, " ", True) + print "" + + print "\n\n\n" + warningStats = [] + for warningNumber in warningCounts.keys(): + warningCount = warningCounts[warningNumber] + if warningNumber in oldWarningCounts: + warningDiff = warningCount - oldWarningCounts[warningNumber] + else: + warningDiff = warningCount + warningStats.append((warningCount, warningNumber, warningDiff)) + for warningNumber in oldWarningCounts.keys(): + if not warningNumber in warningCounts: + warningStats.append((0, warningNumber, -oldWarningCounts[warningNumber])) + warningStats.sort() + warningStats.reverse() + for warningStat in warningStats: + warningNumber = warningStat[1] + description = "" + if warningNumber in warningsToText: + description = ", %s" % warningsToText[warningNumber] + else: + # Replace warning/error with wrning/eror so that these warning summaries don't trigger the + # warning detection logic. + description = ", example: %s" % sampleWarnings[warningNumber].replace("warning", "wrning").replace("error", "eror") + print "%3d occurrences of C%d, changed %d%s" % (warningStat[0], warningStat[1], warningStat[2], description) + + # Print a summary of all stack related warnings in the new data, regardless of whether they were in the old. + bigStackCulprits = {} + allocaCulprits = {} + # c:\src\simplify.cpp(1840): warning C6262: : Function uses '28708' bytes of stack: exceeds /analyze:stacksize'16384'. Consider moving some data to heap + stackUsedRe = re.compile("(.*): warning C6262: Function uses '(\d*)' .*") + print "\n\n\n" + print "Stack related summary:" + print "C6263: Using _alloca in a loop: this can quickly overflow stack" + bigStackCulprits = [] + for key in new.keys(): + # warning C6262: Function uses '400352' bytes of stack + # warning C6263: Using _alloca in a loop + stackMatch = parseKeyRe.match(key) + if stackMatch: + warningNumber = stackMatch.groups()[1] + if warningNumber == "6262": + #print "Found warning %s in %s" % (warningNumber, stackMatch.groups()[2]) + entries = new[key] + printed = {} + for entry in entries: + if not entry in printed: + match = stackUsedRe.match(entry) + if match: + location = match.groups()[0] + stackBytes = int(match.groups()[1]) + printed[entry] = True + bigStackCulprits.append((stackBytes, location)) + elif warningNumber == "6263": + #print "Found warning %s in %s" % (warningNumber, stackMatch.groups()[2]) + entries = new[key] + printed = {} + for entry in entries: + if not entry in printed: + print Cleanup(entry[:entry.find(": ")]) + printed[entry] = True + + print "\n\n" + print "C6262: Functions that use many bytes of stack" + bigStackCulprits.sort() + bigStackCulprits.reverse() + print "filename(linenumber): bytes" + # Print a sorted summary of functions using excessive stack. It would be tidier + # to print the size first (better alignment) but then the output can't be used + # in the Visual Studio output window to jump to the code in question. + + # Get the lengths of all of the file names + lengths = [] + for val in bigStackCulprits: + lengths.append(len(Cleanup(val[1]))) + lengths.sort() + if len(lengths) > 0: + # Set the length at the 9xth percentile so that most of the sizes + # are lined up. + formatLength = lengths[int(len(lengths)*.97)] + formatString = "%%-%ds: %%7d" % formatLength + for val in bigStackCulprits: + print formatString % (Cleanup(val[1]), val[0]) + + # Print a list of all of the outstanding warnings + print "\n\n\n" + print "Outstanding warnings are:" + DumpWarnings(new, True) + return (errorCode, fatalWarningsFound) + + + +def DumpWarnings(new, ignoreInformational): + filePrinted = {} + # If we just scan the dictionary then warnings will be grouped + # by warning-number-in-file, but different warning numbers from the + # same file will be scattered, and different files from the same + # directory will also be scattered. + # We really want warnings sorted by path name. To do that we scan + # through the dictionary and add all of the entries to a dictionary + # whose primary key is filename (path). Then we sort those keys. + warningsByFile = {} + for key in new.keys(): + match = parseKeyRe.match(key) + type, warningNumber, filename = match.groups() + if filename in warningsByFile: + warningsByFile[filename].append(key) + else: + warningsByFile[filename] = [key] + + filenames = warningsByFile.keys() + filenames.sort(); + + for filename in filenames: + for key in warningsByFile[filename]: + match = parseKeyRe.match(key) + warningNumber = match.groups()[1] + if ignoreInformational and warningNumber in informationalWarnings: + pass + else: + newEntries = new[key] + print "%d times:" % len(newEntries) + PrintEntries(newEntries, " ", True) + print "" + + if ignoreInformational: + # Print the 6244 and 6246 warnings together in a group. We print + # them here so that they are sorted by file name. + print "\n\n\nVariable shadowing warnings" + for filename in filenames: + for key in warningsByFile[filename]: + match = parseKeyRe.match(key) + warningNumber = match.groups()[1] + if warningNumber == "6244" or warningNumber == "6246": + newEntries = new[key] + PrintEntries(newEntries, " ", True) + print "" + + + +def GetLogFileName(arg): + # Special indicator for last-known-good. This means that the script + # should look for analysislkg.txt and extract a file name from it. + # Temporarily have "2" be equivalent to "lkg" to allow for a transition + # to the lkg model. + if arg == "lkg" or arg == "2": + try: + lines = open(lkgFilename).readlines() + if len(lines) > 0: + result = lines[0].strip() + print "LKG analysis results are in '%s'" % result + return result + else: + print "No data found in %s" % lkgFilename + except IOError: + print "Failed to open %s" % lkgFilename + arg = 2 + + try: + x = int(arg) + except: + return arg + + if x <= 0: + print "Numerical arguments must be from 1 to numlogs (%s)" % arg + sys.exit(10) + basedir = r"." + dirEntries = os.listdir(basedir) + logRe = re.compile(r"analyze(.*)_cl_(\d+).txt"); + logs = [] + for entry in dirEntries: + if logRe.match(entry): + logs.append(entry) + # This will throw an exception if there aren't enough log files + # available. + newname = os.path.join(basedir, logs[-x]) + return newname + + + +if len(sys.argv) < 2: + print "Usage:" + print "To get a comparison between two error log files:" + print " Syntax: parseerrors newlogfile oldlogfile" + print "To get a summary of a single log file:" + print " Syntax: parseerrors logfile" + print "To get a summary of the two most recent log files:" + print " Syntax: parseerrors 1 2" + print "Log files can also be indicated by number where '1' is the" + print "most recent, '2' is second oldest, etc." + sys.exit(0) + +newname = GetLogFileName(sys.argv[1]) +resultnew = ParseLog(newname) +if len(sys.argv) >= 3: + oldname = GetLogFileName(sys.argv[2]) + resultold = ParseLog(oldname) + result = DumpNewWarnings(resultold, resultnew, oldname, newname) + errorCode = result[0] + fatalWarningsFound = result[1] + if fatalWarningsFound == 0: + if analyzeconfig.updateLastKnownGood: + print "Updating last-known-good." + lkgOutput = open(lkgFilename, "wt") + lkgOutput.write(newname) + else: + print "Updating last-known-good is disabled." + sys.exit(errorCode) +else: + DumpWarnings(resultnew, False) |