[clang] 98f737f - [analyzer] CmpRuns.py: Refactor and add type annotations. NFC.

Valeriy Savchenko via cfe-commits cfe-commits at lists.llvm.org
Tue Jun 16 03:31:13 PDT 2020


Author: Valeriy Savchenko
Date: 2020-06-16T13:29:46+03:00
New Revision: 98f737f4bfc79acac31d899886ad6b5d44396bde

URL: https://github.com/llvm/llvm-project/commit/98f737f4bfc79acac31d899886ad6b5d44396bde
DIFF: https://github.com/llvm/llvm-project/commit/98f737f4bfc79acac31d899886ad6b5d44396bde.diff

LOG: [analyzer] CmpRuns.py: Refactor and add type annotations. NFC.

Differential Revision: https://reviews.llvm.org/D80517

Added: 
    

Modified: 
    clang/utils/analyzer/CmpRuns.py
    clang/utils/analyzer/SATestBuild.py

Removed: 
    


################################################################################
diff  --git a/clang/utils/analyzer/CmpRuns.py b/clang/utils/analyzer/CmpRuns.py
index 28e9258f593a..d94d0bfc83b5 100755
--- a/clang/utils/analyzer/CmpRuns.py
+++ b/clang/utils/analyzer/CmpRuns.py
@@ -17,26 +17,35 @@
     # Load the results of both runs, to obtain lists of the corresponding
     # AnalysisDiagnostic objects.
     #
-    resultsA = loadResultsFromSingleRun(singleRunInfoA, deleteEmpty)
-    resultsB = loadResultsFromSingleRun(singleRunInfoB, deleteEmpty)
+    resultsA = load_results_from_single_run(singleRunInfoA, delete_empty)
+    resultsB = load_results_from_single_run(singleRunInfoB, delete_empty)
 
     # Generate a relation from diagnostics in run A to diagnostics in run B
     # to obtain a list of triples (a, b, confidence).
-    
diff  = compareResults(resultsA, resultsB)
+    
diff  = compare_results(resultsA, resultsB)
 
 """
-from __future__ import division, print_function
-
-from collections import defaultdict
-
-from math import log
-from optparse import OptionParser
+import argparse
 import json
 import os
 import plistlib
 import re
 import sys
 
+from math import log
+from collections import defaultdict
+from copy import copy
+from typing import (Any, cast, Dict, List, Optional, Sequence, TextIO, TypeVar,
+                    Tuple, Union)
+
+
+Number = Union[int, float]
+Stats = Dict[str, Dict[str, Number]]
+Plist = Dict[str, Any]
+JSON = Dict[str, Any]
+# Type for generics
+T = TypeVar('T')
+
 STATS_REGEXP = re.compile(r"Statistics: (\{.+\})", re.MULTILINE | re.DOTALL)
 
 
@@ -56,118 +65,127 @@ class SingleRunInfo:
     root - the name of the root directory, which will be disregarded when
     determining the source file name
     """
-    def __init__(self, path, root="", verboseLog=None):
+    def __init__(self, path: str, root: str = "", verbose_log=None):
         self.path = path
         self.root = root.rstrip("/\\")
-        self.verboseLog = verboseLog
+        self.verbose_log = verbose_log
 
 
 class AnalysisDiagnostic:
-    def __init__(self, data, report, htmlReport):
+    def __init__(self, data: Plist, report: "AnalysisReport",
+                 html_report: Optional[str]):
         self._data = data
         self._loc = self._data['location']
         self._report = report
-        self._htmlReport = htmlReport
-        self._reportSize = len(self._data['path'])
+        self._html_report = html_report
+        self._report_size = len(self._data['path'])
 
-    def getFileName(self):
+    def get_file_name(self) -> str:
         root = self._report.run.root
-        fileName = self._report.files[self._loc['file']]
-        if fileName.startswith(root) and len(root) > 0:
-            return fileName[len(root) + 1:]
-        return fileName
+        file_name = self._report.files[self._loc['file']]
+
+        if file_name.startswith(root) and len(root) > 0:
+            return file_name[len(root) + 1:]
 
-    def getRootFileName(self):
+        return file_name
+
+    def get_root_file_name(self) -> str:
         path = self._data['path']
+
         if not path:
-            return self.getFileName()
+            return self.get_file_name()
+
         p = path[0]
         if 'location' in p:
-            fIdx = p['location']['file']
+            file_index = p['location']['file']
         else:  # control edge
-            fIdx = path[0]['edges'][0]['start'][0]['file']
-        out = self._report.files[fIdx]
+            file_index = path[0]['edges'][0]['start'][0]['file']
+
+        out = self._report.files[file_index]
         root = self._report.run.root
+
         if out.startswith(root):
             return out[len(root):]
+
         return out
 
-    def getLine(self):
+    def get_line(self) -> int:
         return self._loc['line']
 
-    def getColumn(self):
+    def get_column(self) -> int:
         return self._loc['col']
 
-    def getPathLength(self):
-        return self._reportSize
+    def get_path_length(self) -> int:
+        return self._report_size
 
-    def getCategory(self):
+    def get_category(self) -> str:
         return self._data['category']
 
-    def getDescription(self):
+    def get_description(self) -> str:
         return self._data['description']
 
-    def getIssueIdentifier(self):
-        id = self.getFileName() + "+"
-        if 'issue_context' in self._data:
-            id += self._data['issue_context'] + "+"
-        if 'issue_hash_content_of_line_in_context' in self._data:
-            id += str(self._data['issue_hash_content_of_line_in_context'])
+    def get_issue_identifier(self) -> str:
+        id = self.get_file_name() + "+"
+
+        if "issue_context" in self._data:
+            id += self._data["issue_context"] + "+"
+
+        if "issue_hash_content_of_line_in_context" in self._data:
+            id += str(self._data["issue_hash_content_of_line_in_context"])
+
         return id
 
-    def getReport(self):
-        if self._htmlReport is None:
+    def get_html_report(self) -> str:
+        if self._html_report is None:
             return " "
-        return os.path.join(self._report.run.path, self._htmlReport)
 
-    def getReadableName(self):
-        if 'issue_context' in self._data:
-            funcnamePostfix = "#" + self._data['issue_context']
+        return os.path.join(self._report.run.path, self._html_report)
+
+    def get_readable_name(self) -> str:
+        if "issue_context" in self._data:
+            funcname_postfix = "#" + self._data["issue_context"]
         else:
-            funcnamePostfix = ""
-        rootFilename = self.getRootFileName()
-        fileName = self.getFileName()
-        if rootFilename != fileName:
-            filePrefix = "[%s] %s" % (rootFilename, fileName)
+            funcname_postfix = ""
+
+        root_filename = self.get_root_file_name()
+        file_name = self.get_file_name()
+
+        if root_filename != file_name:
+            file_prefix = f"[{root_filename}] {file_name}"
         else:
-            filePrefix = rootFilename
-        return '%s%s:%d:%d, %s: %s' % (filePrefix,
-                                       funcnamePostfix,
-                                       self.getLine(),
-                                       self.getColumn(), self.getCategory(),
-                                       self.getDescription())
+            file_prefix = root_filename
+
+        line = self.get_line()
+        col = self.get_column()
+        return f"{file_prefix}{funcname_postfix}:{line}:{col}" \
+            f", {self.get_category()}: {self.get_description()}"
 
     # Note, the data format is not an API and may change from one analyzer
     # version to another.
-    def getRawData(self):
+    def get_raw_data(self) -> Plist:
         return self._data
 
 
-class AnalysisReport:
-    def __init__(self, run, files):
-        self.run = run
-        self.files = files
-        self.diagnostics = []
-
-
 class AnalysisRun:
-    def __init__(self, info):
+    def __init__(self, info: SingleRunInfo):
         self.path = info.path
         self.root = info.root
         self.info = info
-        self.reports = []
+        self.reports: List[AnalysisReport] = []
         # Cumulative list of all diagnostics from all the reports.
-        self.diagnostics = []
-        self.clang_version = None
-        self.stats = []
+        self.diagnostics: List[AnalysisDiagnostic] = []
+        self.clang_version: Optional[str] = None
+        self.raw_stats: List[JSON] = []
 
-    def getClangVersion(self):
+    def get_clang_version(self) -> Optional[str]:
         return self.clang_version
 
-    def readSingleFile(self, p, deleteEmpty):
-        data = plistlib.readPlist(p)
+    def read_single_file(self, path: str, delete_empty: bool):
+        with open(path, "rb") as plist_file:
+            data = plistlib.load(plist_file)
+
         if 'statistics' in data:
-            self.stats.append(json.loads(data['statistics']))
+            self.raw_stats.append(json.loads(data['statistics']))
             data.pop('statistics')
 
         # We want to retrieve the clang version even if there are no
@@ -181,8 +199,8 @@ def readSingleFile(self, p, deleteEmpty):
 
         # Ignore/delete empty reports.
         if not data['files']:
-            if deleteEmpty:
-                os.remove(p)
+            if delete_empty:
+                os.remove(path)
             return
 
         # Extract the HTML reports, if they exists.
@@ -207,86 +225,116 @@ def readSingleFile(self, p, deleteEmpty):
         self.diagnostics.extend(diagnostics)
 
 
-def loadResults(path, opts, root="", deleteEmpty=True):
+class AnalysisReport:
+    def __init__(self, run: AnalysisRun, files: List[str]):
+        self.run = run
+        self.files = files
+        self.diagnostics: List[AnalysisDiagnostic] = []
+
+
+def load_results(path: str, args: argparse.Namespace, root: str = "",
+                 delete_empty: bool = True) -> AnalysisRun:
     """
     Backwards compatibility API.
     """
-    return loadResultsFromSingleRun(SingleRunInfo(path, root, opts.verboseLog),
-                                    deleteEmpty)
+    return load_results_from_single_run(SingleRunInfo(path, root,
+                                                      args.verbose_log),
+                                        delete_empty)
 
 
-def loadResultsFromSingleRun(info, deleteEmpty=True):
+def load_results_from_single_run(info: SingleRunInfo,
+                                 delete_empty: bool = True) -> AnalysisRun:
     """
     # Load results of the analyzes from a given output folder.
     # - info is the SingleRunInfo object
-    # - deleteEmpty specifies if the empty plist files should be deleted
+    # - delete_empty specifies if the empty plist files should be deleted
 
     """
     path = info.path
     run = AnalysisRun(info)
 
     if os.path.isfile(path):
-        run.readSingleFile(path, deleteEmpty)
+        run.read_single_file(path, delete_empty)
     else:
-        for (dirpath, dirnames, filenames) in os.walk(path):
+        for dirpath, dirnames, filenames in os.walk(path):
             for f in filenames:
-                if (not f.endswith('plist')):
+                if not f.endswith('plist'):
                     continue
+
                 p = os.path.join(dirpath, f)
-                run.readSingleFile(p, deleteEmpty)
+                run.read_single_file(p, delete_empty)
 
     return run
 
 
-def cmpAnalysisDiagnostic(d):
-    return d.getIssueIdentifier()
+def cmp_analysis_diagnostic(d):
+    return d.get_issue_identifier()
 
 
-def compareResults(A, B, opts):
+PresentInBoth = Tuple[AnalysisDiagnostic, AnalysisDiagnostic]
+PresentOnlyInOld = Tuple[AnalysisDiagnostic, None]
+PresentOnlyInNew = Tuple[None, AnalysisDiagnostic]
+ComparisonResult = List[Union[PresentInBoth,
+                              PresentOnlyInOld,
+                              PresentOnlyInNew]]
+
+
+def compare_results(results_old: AnalysisRun, results_new: AnalysisRun,
+                    args: argparse.Namespace) -> ComparisonResult:
     """
-    compareResults - Generate a relation from diagnostics in run A to
+    compare_results - Generate a relation from diagnostics in run A to
     diagnostics in run B.
 
     The result is the relation as a list of triples (a, b) where
     each element {a,b} is None or a matching element from the respective run
     """
 
-    res = []
+    res: ComparisonResult = []
 
     # Map size_before -> size_after
-    path_
diff erence_data = []
+    path_
diff erence_data: List[float] = []
 
     # Quickly eliminate equal elements.
-    neqA = []
-    neqB = []
-    eltsA = list(A.diagnostics)
-    eltsB = list(B.diagnostics)
-    eltsA.sort(key=cmpAnalysisDiagnostic)
-    eltsB.sort(key=cmpAnalysisDiagnostic)
-    while eltsA and eltsB:
-        a = eltsA.pop()
-        b = eltsB.pop()
-        if (a.getIssueIdentifier() == b.getIssueIdentifier()):
-            if a.getPathLength() != b.getPathLength():
-                if opts.relative_path_histogram:
+    neq_old: List[AnalysisDiagnostic] = []
+    neq_new: List[AnalysisDiagnostic] = []
+
+    diags_old = copy(results_old.diagnostics)
+    diags_new = copy(results_new.diagnostics)
+
+    diags_old.sort(key=cmp_analysis_diagnostic)
+    diags_new.sort(key=cmp_analysis_diagnostic)
+
+    while diags_old and diags_new:
+        a = diags_old.pop()
+        b = diags_new.pop()
+
+        if a.get_issue_identifier() == b.get_issue_identifier():
+            if a.get_path_length() != b.get_path_length():
+
+                if args.relative_path_histogram:
                     path_
diff erence_data.append(
-                        float(a.getPathLength()) / b.getPathLength())
-                elif opts.relative_log_path_histogram:
+                        float(a.get_path_length()) / b.get_path_length())
+
+                elif args.relative_log_path_histogram:
                     path_
diff erence_data.append(
-                        log(float(a.getPathLength()) / b.getPathLength()))
-                elif opts.absolute_path_histogram:
+                        log(float(a.get_path_length()) / b.get_path_length()))
+
+                elif args.absolute_path_histogram:
                     path_
diff erence_data.append(
-                        a.getPathLength() - b.getPathLength())
+                        a.get_path_length() - b.get_path_length())
 
             res.append((a, b))
-        elif a.getIssueIdentifier() > b.getIssueIdentifier():
-            eltsB.append(b)
-            neqA.append(a)
+
+        elif a.get_issue_identifier() > b.get_issue_identifier():
+            diags_new.append(b)
+            neq_old.append(a)
+
         else:
-            eltsA.append(a)
-            neqB.append(b)
-    neqA.extend(eltsA)
-    neqB.extend(eltsB)
+            diags_old.append(a)
+            neq_new.append(b)
+
+    neq_old.extend(diags_old)
+    neq_new.extend(diags_new)
 
     # FIXME: Add fuzzy matching. One simple and possible effective idea would
     # be to bin the diagnostics, print them in a normalized form (based solely
@@ -294,13 +342,13 @@ def compareResults(A, B, opts):
     # the basis for matching. This has the nice property that we don't depend
     # in any way on the diagnostic format.
 
-    for a in neqA:
+    for a in neq_old:
         res.append((a, None))
-    for b in neqB:
+    for b in neq_new:
         res.append((None, b))
 
-    if opts.relative_log_path_histogram or opts.relative_path_histogram or \
-            opts.absolute_path_histogram:
+    if args.relative_log_path_histogram or args.relative_path_histogram or \
+            args.absolute_path_histogram:
         from matplotlib import pyplot
         pyplot.hist(path_
diff erence_data, bins=100)
         pyplot.show()
@@ -308,156 +356,184 @@ def compareResults(A, B, opts):
     return res
 
 
-def computePercentile(l, percentile):
+def compute_percentile(values: Sequence[T], percentile: float) -> T:
     """
     Return computed percentile.
     """
-    return sorted(l)[int(round(percentile * len(l) + 0.5)) - 1]
+    return sorted(values)[int(round(percentile * len(values) + 0.5)) - 1]
 
 
-def deriveStats(results):
+def derive_stats(results: AnalysisRun) -> Stats:
     # Assume all keys are the same in each statistics bucket.
     combined_data = defaultdict(list)
 
     # Collect data on paths length.
     for report in results.reports:
         for diagnostic in report.diagnostics:
-            combined_data['PathsLength'].append(diagnostic.getPathLength())
+            combined_data['PathsLength'].append(diagnostic.get_path_length())
 
-    for stat in results.stats:
+    for stat in results.raw_stats:
         for key, value in stat.items():
-            combined_data[key].append(value)
-    combined_stats = {}
+            combined_data[str(key)].append(value)
+
+    combined_stats: Stats = {}
+
     for key, values in combined_data.items():
-        combined_stats[str(key)] = {
+        combined_stats[key] = {
             "max": max(values),
             "min": min(values),
             "mean": sum(values) / len(values),
-            "90th %tile": computePercentile(values, 0.9),
-            "95th %tile": computePercentile(values, 0.95),
+            "90th %tile": compute_percentile(values, 0.9),
+            "95th %tile": compute_percentile(values, 0.95),
             "median": sorted(values)[len(values) // 2],
             "total": sum(values)
         }
+
     return combined_stats
 
 
-def compareStats(resultsA, resultsB):
-    statsA = deriveStats(resultsA)
-    statsB = deriveStats(resultsB)
-    keys = sorted(statsA.keys())
+# TODO: compare_results decouples comparison from the output, we should
+#       do it here as well
+def compare_stats(results_old: AnalysisRun, results_new: AnalysisRun):
+    stats_old = derive_stats(results_old)
+    stats_new = derive_stats(results_new)
+
+    keys = sorted(stats_old.keys())
+
+    # FIXME: stats_old and stats_new are not necessarily sharing all of their
+    #        stats and can crash when stats_new doesn't have/removed some
     for key in keys:
         print(key)
-        for kkey in statsA[key]:
-            valA = float(statsA[key][kkey])
-            valB = float(statsB[key][kkey])
-            report = "%.3f -> %.3f" % (valA, valB)
+
+        for kkey in stats_old[key]:
+            val_old = float(stats_old[key][kkey])
+            val_new = float(stats_new[key][kkey])
+
+            report = f"{val_old:.3f} -> {val_new:.3f}"
+
             # Only apply highlighting when writing to TTY and it's not Windows
             if sys.stdout.isatty() and os.name != 'nt':
-                if valB != 0:
-                    ratio = (valB - valA) / valB
+                if val_new != 0:
+                    ratio = (val_new - val_old) / val_new
                     if ratio < -0.2:
                         report = Colors.GREEN + report + Colors.CLEAR
                     elif ratio > 0.2:
                         report = Colors.RED + report + Colors.CLEAR
-            print("\t %s %s" % (kkey, report))
 
+            print(f"\t {kkey} {report}")
 
-def dumpScanBuildResultsDiff(dirA, dirB, opts, deleteEmpty=True,
-                             Stdout=sys.stdout):
+
+def dump_scan_build_results_
diff (dir_old: str, dir_new: str,
+                                 args: argparse.Namespace,
+                                 delete_empty: bool = True,
+                                 out: TextIO = sys.stdout):
     # Load the run results.
-    resultsA = loadResults(dirA, opts, opts.rootA, deleteEmpty)
-    resultsB = loadResults(dirB, opts, opts.rootB, deleteEmpty)
-    if opts.show_stats:
-        compareStats(resultsA, resultsB)
-    if opts.stats_only:
+    results_old = load_results(dir_old, args, args.root_old, delete_empty)
+    results_new = load_results(dir_new, args, args.root_new, delete_empty)
+
+    if args.show_stats:
+        compare_stats(results_old, results_new)
+    if args.stats_only:
         return
 
     # Open the verbose log, if given.
-    if opts.verboseLog:
-        auxLog = open(opts.verboseLog, "w")
+    if args.verbose_log:
+        auxLog: Optional[TextIO] = open(args.verbose_log, "w")
     else:
         auxLog = None
 
-    
diff  = compareResults(resultsA, resultsB, opts)
-    foundDiffs = 0
-    totalAdded = 0
-    totalRemoved = 0
+    
diff  = compare_results(results_old, results_new, args)
+    found_
diff s = 0
+    total_added = 0
+    total_removed = 0
+
     for res in 
diff :
-        a, b = res
-        if a is None:
-            Stdout.write("ADDED: %r\n" % b.getReadableName())
-            foundDiffs += 1
-            totalAdded += 1
+        old, new = res
+        if old is None:
+            # TODO: mypy still doesn't understand that old and new can't be
+            #       both Nones, we should introduce a better type solution
+            new = cast(AnalysisDiagnostic, new)
+            out.write(f"ADDED: {new.get_readable_name()}\n")
+            found_
diff s += 1
+            total_added += 1
             if auxLog:
-                auxLog.write("('ADDED', %r, %r)\n" % (b.getReadableName(),
-                                                      b.getReport()))
-        elif b is None:
-            Stdout.write("REMOVED: %r\n" % a.getReadableName())
-            foundDiffs += 1
-            totalRemoved += 1
+                auxLog.write(f"('ADDED', {new.get_readable_name()}, "
+                             f"{new.get_html_report()})\n")
+
+        elif new is None:
+            out.write(f"REMOVED: {old.get_readable_name()}\n")
+            found_
diff s += 1
+            total_removed += 1
             if auxLog:
-                auxLog.write("('REMOVED', %r, %r)\n" % (a.getReadableName(),
-                                                        a.getReport()))
+                auxLog.write(f"('REMOVED', {old.get_readable_name()}, "
+                             f"{old.get_html_report()})\n")
         else:
             pass
 
-    TotalReports = len(resultsB.diagnostics)
-    Stdout.write("TOTAL REPORTS: %r\n" % TotalReports)
-    Stdout.write("TOTAL ADDED: %r\n" % totalAdded)
-    Stdout.write("TOTAL REMOVED: %r\n" % totalRemoved)
+    total_reports = len(results_new.diagnostics)
+    out.write(f"TOTAL REPORTS: {total_reports}\n")
+    out.write(f"TOTAL ADDED: {total_added}\n")
+    out.write(f"TOTAL REMOVED: {total_removed}\n")
+
     if auxLog:
-        auxLog.write("('TOTAL NEW REPORTS', %r)\n" % TotalReports)
-        auxLog.write("('TOTAL DIFFERENCES', %r)\n" % foundDiffs)
+        auxLog.write(f"('TOTAL NEW REPORTS', {total_reports})\n")
+        auxLog.write(f"('TOTAL DIFFERENCES', {found_
diff s})\n")
         auxLog.close()
 
-    return foundDiffs, len(resultsA.diagnostics), len(resultsB.diagnostics)
+    # TODO: change to NamedTuple
+    return found_
diff s, len(results_old.diagnostics), \
+        len(results_new.diagnostics)
 
 
 def generate_option_parser():
-    parser = OptionParser("usage: %prog [options] [dir A] [dir B]")
-    parser.add_option("", "--rootA", dest="rootA",
-                      help="Prefix to ignore on source files for directory A",
-                      action="store", type=str, default="")
-    parser.add_option("", "--rootB", dest="rootB",
-                      help="Prefix to ignore on source files for directory B",
-                      action="store", type=str, default="")
-    parser.add_option("", "--verbose-log", dest="verboseLog",
-                      help="Write additional information to LOG \
-                           [default=None]",
-                      action="store", type=str, default=None,
-                      metavar="LOG")
-    parser.add_option("--relative-path-
diff erences-histogram",
-                      action="store_true", dest="relative_path_histogram",
-                      default=False,
-                      help="Show histogram of relative paths 
diff erences. \
-                            Requires matplotlib")
-    parser.add_option("--relative-log-path-
diff erences-histogram",
-                      action="store_true", dest="relative_log_path_histogram",
-                      default=False,
-                      help="Show histogram of log relative paths 
diff erences. \
-                            Requires matplotlib")
-    parser.add_option("--absolute-path-
diff erences-histogram",
-                      action="store_true", dest="absolute_path_histogram",
-                      default=False,
-                      help="Show histogram of absolute paths 
diff erences. \
-                            Requires matplotlib")
-    parser.add_option("--stats-only", action="store_true", dest="stats_only",
-                      default=False, help="Only show statistics on reports")
-    parser.add_option("--show-stats", action="store_true", dest="show_stats",
-                      default=False, help="Show change in statistics")
+    parser = argparse.ArgumentParser()
+
+    parser.add_argument("--root-old", dest="root_old",
+                        help="Prefix to ignore on source files for "
+                        "OLD directory",
+                        action="store", type=str, default="")
+    parser.add_argument("--root-new", dest="root_new",
+                        help="Prefix to ignore on source files for "
+                        "NEW directory",
+                        action="store", type=str, default="")
+    parser.add_argument("--verbose-log", dest="verbose_log",
+                        help="Write additional information to LOG "
+                        "[default=None]",
+                        action="store", type=str, default=None,
+                        metavar="LOG")
+    parser.add_argument("--relative-path-
diff erences-histogram",
+                        action="store_true", dest="relative_path_histogram",
+                        default=False,
+                        help="Show histogram of relative paths 
diff erences. "
+                        "Requires matplotlib")
+    parser.add_argument("--relative-log-path-
diff erences-histogram",
+                        action="store_true",
+                        dest="relative_log_path_histogram", default=False,
+                        help="Show histogram of log relative paths "
+                        "
diff erences. Requires matplotlib")
+    parser.add_argument("--absolute-path-
diff erences-histogram",
+                        action="store_true", dest="absolute_path_histogram",
+                        default=False,
+                        help="Show histogram of absolute paths 
diff erences. "
+                        "Requires matplotlib")
+    parser.add_argument("--stats-only", action="store_true", dest="stats_only",
+                        default=False, help="Only show statistics on reports")
+    parser.add_argument("--show-stats", action="store_true", dest="show_stats",
+                        default=False, help="Show change in statistics")
+    parser.add_argument("old", nargs=1, help="Directory with old results")
+    parser.add_argument("new", nargs=1, help="Directory with new results")
+
     return parser
 
 
 def main():
     parser = generate_option_parser()
-    (opts, args) = parser.parse_args()
-
-    if len(args) != 2:
-        parser.error("invalid number of arguments")
+    args = parser.parse_args()
 
-    dirA, dirB = args
+    dir_old = args.old[0]
+    dir_new = args.new[0]
 
-    dumpScanBuildResultsDiff(dirA, dirB, opts)
+    dump_scan_build_results_
diff (dir_old, dir_new, args)
 
 
 if __name__ == '__main__':

diff  --git a/clang/utils/analyzer/SATestBuild.py b/clang/utils/analyzer/SATestBuild.py
index 5ff430d5fcf3..c7f28abf9654 100755
--- a/clang/utils/analyzer/SATestBuild.py
+++ b/clang/utils/analyzer/SATestBuild.py
@@ -750,13 +750,13 @@ def run_cmp_results(directory: str, strictness: int = 0) -> bool:
         patched_source = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
 
         # TODO: get rid of option parser invocation here
-        opts, args = CmpRuns.generate_option_parser().parse_args(
-            ["--rootA", "", "--rootB", patched_source])
+        args = CmpRuns.generate_option_parser().parse_args(
+            ["--root-old", "", "--root-new", patched_source, "", ""])
         # Scan the results, delete empty plist files.
         num_
diff s, reports_in_ref, reports_in_new = \
-            CmpRuns.dumpScanBuildResultsDiff(ref_dir, new_dir, opts,
-                                             deleteEmpty=False,
-                                             Stdout=LOCAL.stdout)
+            CmpRuns.dump_scan_build_results_
diff (ref_dir, new_dir, args,
+                                                 delete_empty=False,
+                                                 out=LOCAL.stdout)
 
         if num_
diff s > 0:
             stdout(f"Warning: {num_
diff s} 
diff erences in diagnostics.\n")
@@ -834,7 +834,8 @@ def clean_up_empty_plists(output_dir: str):
         plist = os.path.join(output_dir, plist)
 
         try:
-            data = plistlib.readPlist(plist)
+            with open(plist, "rb") as plist_file:
+                data = plistlib.load(plist_file)
             # Delete empty reports.
             if not data['files']:
                 os.remove(plist)


        


More information about the cfe-commits mailing list