Skip to content
scan-build 15.6 KiB
Newer Older
#!/usr/bin/env perl
#
#                     The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.
#
##===----------------------------------------------------------------------===##
#
# A script designed to wrap a build so that all calls to gcc are intercepted
# and piped to the static analyzer.
#
##===----------------------------------------------------------------------===##

use strict;
use warnings;
use File::Temp qw/ :mktemp /;
use FindBin qw($RealBin);

my $Verbose = 0;       # Verbose output from this script.
my $Prog = "scan-build";

##----------------------------------------------------------------------------##
# GetHTMLRunDir - Construct an HTML directory name for the current run.
##----------------------------------------------------------------------------##

Sam Bishop's avatar
Sam Bishop committed
sub GetHTMLRunDir {  

  die "Not enough arguments." if (@_ == 0);
  
  my $Dir = shift @_;
  
  # Get current date and time.
  
  my @CurrentTime = localtime();
  
  my $year  = $CurrentTime[5] + 1900;
  my $day   = $CurrentTime[3];
  my $month = $CurrentTime[4] + 1;
  
  my $DateString = "$year-$month-$day";
  
  # Determine the run number.
  
  my $RunNumber;
  
  if (-d $Dir) {
    
    if (! -r $Dir) {
Sam Bishop's avatar
Sam Bishop committed
      die "error: '$Dir' exists but is not readable.\n";
    }
    
    # Iterate over all files in the specified directory.
    
    my $max = 0;
    
    opendir(DIR, $Dir);
    my @FILES= readdir(DIR); 
    closedir(DIR);
    
    foreach my $f (@FILES) {

      my @x = split/-/, $f;
      
      next if (scalar(@x) != 4);
      next if ($x[0] != $year);
      next if ($x[1] != $month);
      next if ($x[2] != $day);
      
      if ($x[3] > $max) {
        $max = $x[3];
      }      
    }
    
    $RunNumber = $max + 1;
  }
  else {
    
    if (-x $Dir) {
Sam Bishop's avatar
Sam Bishop committed
      die "error: '$Dir' exists but is not a directory.\n";
    }
    
    # $Dir does not exist.  It will be automatically created by the 
    # clang driver.  Set the run number to 1.  
    
    $RunNumber = 1;
  }
  
  die "RunNumber must be defined!" if (!defined($RunNumber));
  
  # Append the run number.
  
  return "$Dir/$DateString-$RunNumber";  
}

Sam Bishop's avatar
Sam Bishop committed
sub SetHtmlEnv {
  
  die "Wrong number of arguments." if (scalar(@_) != 2);
  
  my $Args = shift;
  my $Dir = shift;
  
  die "No build command." if (scalar(@$Args) == 0);
  
  my $Cmd = $$Args[0];
  
  if ($Cmd =~ /configure/) {
    return;
  }
  
  if ($Verbose) {
    print "$Prog: Emitting reports for this run to '$Dir'.\n";  
  }
  
  $ENV{'CCC_ANALYZER_HTML'} = $Dir;
}

##----------------------------------------------------------------------------##
# ComputeDigest - Compute a digest of the specified file.
##----------------------------------------------------------------------------##

sub ComputeDigest {
  my $FName = shift;
  die "Cannot read $FName" if (! -r $FName);  
  
  # Use Digest::MD5.  We don't have to be cryptographically secure.  We're
  # just looking for duplicate files that come from a non-malicious source.
  # We use Digest::MD5 because it is a standard Perl module that should
  # come bundled on most systems.
  
  open(FILE, $FName) or die "Cannot open $FName.";
  binmode FILE;
  my $Result = Digest::MD5->new->addfile(*FILE)->hexdigest;
  close(FILE);
  
  # Return the digest.
  
  return $Result;
##----------------------------------------------------------------------------##
#  UpdatePrefix - Compute the common prefix of files.
##----------------------------------------------------------------------------##

my $Prefix;

sub UpdatePrefix {
  
  my $x = shift;
  my $y = basename($x);
  $x =~ s/\Q$y\E$//;
  
  # Ignore /usr, /Library, /System, /Developer

  return if ( $x =~ /^\/usr/ or $x =~ /^\/Library/
              or $x =~ /^\/System/ or $x =~ /^\/Developer/);

  
  if (!defined $Prefix) {
    $Prefix = $x;
    return;
  }
  
  chop $Prefix while (!($x =~ /^$Prefix/));
}

sub GetPrefix {
  return $Prefix;
}

##----------------------------------------------------------------------------##
#  UpdateInFilePath - Update the path in the report file.
##----------------------------------------------------------------------------##

sub UpdateInFilePath {
  my $fname = shift;
  my $regex = shift;
  my $newtext = shift;
  
  open (RIN, $fname) or die "cannot open $fname";
  open (ROUT, ">$fname.tmp") or die "cannot open $fname.tmp";
  
  while (<RIN>) {
    s/$regex/$newtext/;
    print ROUT $_;
  }
  
  close (ROUT);
  close (RIN);
  `mv $fname.tmp $fname`;
}

##----------------------------------------------------------------------------##
# ScanFile - Scan a report file for various identifying attributes.
##----------------------------------------------------------------------------##

# Sometimes a source file is scanned more than once, and thus produces
# multiple error reports.  We use a cache to solve this problem.

my %AlreadyScanned;

sub ScanFile {
  
  my $Index = shift;
  my $Dir = shift;
  my $FName = shift;
  
  # Compute a digest for the report file.  Determine if we have already
  # scanned a file that looks just like it.
  
  my $digest = ComputeDigest("$Dir/$FName");

  if (defined($AlreadyScanned{$digest})) {
    # Redundant file.  Remove it.
    `rm -f $Dir/$FName`;
    return;
  }
  
  $AlreadyScanned{$digest} = 1;
  
Ted Kremenek's avatar
Ted Kremenek committed
  # At this point the report file is not world readable.  Make it happen.
  `chmod 644 $Dir/$FName`;
  
  # Scan the report file for tags.
  open(IN, "$Dir/$FName") or die "$Prog: Cannot open '$Dir/$FName'\n";

  my $BugDesc = "";
  my $BugFile = "";
  my $BugPathLength = 1;
  my $BugLine = 0;
  
  while (<IN>) {
    
    if (/<!-- BUGDESC (.*) -->$/) {
      $BugDesc = $1;
    }
    elsif (/<!-- BUGFILE (.*) -->$/) {
      $BugFile = $1;
    }
    elsif (/<!-- BUGPATHLENGTH (.*) -->$/) {
      $BugPathLength = $1;
    }
    elsif (/<!-- BUGLINE (.*) -->$/) {
      $BugLine = $1;    
    }
  push @$Index,[ $FName, $BugDesc, $BugFile, $BugLine, $BugPathLength ];
}

##----------------------------------------------------------------------------##
# CopyJS - Copy JavaScript code to target directory.
##----------------------------------------------------------------------------##

sub CopyJS {

  my $Dir = shift;
  
  die "$Prog: Cannot find 'sorttable.js'.\n"
    if (! -r "$RealBin/sorttable.js");  

  `cp $RealBin/sorttable.js $Dir`;

  die "$Prog: Could not copy 'sorttable.js' to '$Dir'."
    if (! -r "$Dir/sorttable.js");
##----------------------------------------------------------------------------##
# Postprocess - Postprocess the results of an analysis scan.
##----------------------------------------------------------------------------##

Sam Bishop's avatar
Sam Bishop committed
sub Postprocess {
  
  die "No directory specified." if (!defined($Dir));
  die "No base directory specified." if (!defined($BaseDir));
  
  if (! -d $Dir) {
    return;
  }
  
  opendir(DIR, $Dir);
  my @files = grep(/^report-.*\.html$/,readdir(DIR));
  closedir(DIR);

  if (scalar(@files) == 0) {
Ted Kremenek's avatar
Ted Kremenek committed
    print "$Prog: Removing directory '$Dir' because it contains no reports.\n";
  
  # Scan each report file and build an index.
  
  my @Index;
    
  foreach my $file (@files) { ScanFile(\@Index, $Dir, $file); }
  
  # Generate an index.html file.
  
  my $FName = "$Dir/index.html";
  
  open(OUT, ">$FName") or die "$Prog: Cannot create file '$FName'\n";
  
print OUT <<ENDTEXT;
<html>
<head>
<style type="text/css">
 body { color:#000000; background-color:#ffffff }
 body { font-family: Helvetica, sans-serif; font-size:9pt }
 h1 { font-size:12pt }
 table.sortable thead {
   background-color:#eee; color:#666666;
   font-weight: bold; cursor: default;
   text-align:center;
   border-top: 2px solid #000000;
   border-bottom: 2px solid #000000;
   font-weight: bold; font-family: Verdana
 } 
 table.sortable { border: 1px #000000 solid }
 table.sortable { border-collapse: collapse; border-spacing: 0px }
 td { border-bottom: 1px #000000 dotted }
 td { padding:5px; padding-left:8px; padding-right:8px }
 td { text-align:left; font-size:9pt }
 td.View   { padding-left: 10px }
<script src="sorttable.js"></script>
<script language='javascript' type="text/javascript">
function SetDisplay(RowClass, DisplayVal)
{
  var Rows = document.getElementsByTagName("tr");
  for ( var i = 0 ; i < Rows.length; ++i ) {
    if (Rows[i].className == RowClass) {
      Rows[i].style.display = DisplayVal;
    }
  }
}
  
function ToggleDisplay(CheckButton, ClassName) {
  window.console.log("writing");
  if (CheckButton.checked) {
    SetDisplay(ClassName, "");
  }
  else {
    SetDisplay(ClassName, "none");
  }
}
</script>
</head>
<body>
ENDTEXT

  # Print out the summary table.
  
  my %Totals;
  
  for my $row ( @Index ) {
    
    my $bug_type = lc($row->[1]);
    
    if (!defined($Totals{$bug_type})) {
      $Totals{$bug_type} = 1;
    }
    else {
      $Totals{$bug_type}++;
    }
  }

print OUT <<ENDTEXT;
<h3>Summary</h3>
<table class="sortable">
<tr>
  <td>Bug Type</td>
  <td>Quantity</td>
  <td "sorttable_nosort">Display?</td>
</tr>
ENDTEXT
  
  for my $key ( sort { $a cmp $b } keys %Totals ) {
    my $x = $key;
    $x =~ s/\s/_/g;    
    print OUT "<tr><td>$key</td><td>$Totals{$key}</td><td><input type=\"checkbox\" onClick=\"ToggleDisplay(this,'bt_$x');\" checked/></td></tr>\n";
  }

  # Print out the table of errors.

print OUT <<ENDTEXT;
</table>
<h3>Reports</h3>
<table class="sortable">
  <td>Bug Type</td>
  <td>File</td>
  <td>Line</td>
  <td>Path Length</td>
  <td "sorttable_nosort"></td>
  my $prefix = GetPrefix();
  my $regex;
  my $InFileRegex;
  my $InFilePrefix = "File:</td><td>";
  
  if (defined($prefix)) { 
    $regex = qr/^\Q$prefix\E/is;    
    $InFileRegex = qr/\Q$InFilePrefix$prefix\E/is;
  }    

  for my $row ( sort { $a->[1] cmp $b->[1] } @Index ) {
    
    my $x = lc($row->[1]);
    $x =~ s/\s/_/g;    
    
    print OUT "<tr class=\"bt_$x\">\n";

    my $ReportFile = $row->[0];
    print OUT " <td class=\"DESC\">";
    print OUT lc($row->[1]);
    print OUT "</td>\n";
    # Update the file prefix.
    
    my $fname = $row->[2];
    if (defined($regex)) {      
      $fname =~ s/$regex//;
      UpdateInFilePath("$Dir/$ReportFile", $InFileRegex, $InFilePrefix)
    }
    print OUT "<td>$fname</td>\n";

    # Print the rest of the columns.
    
    for my $j ( 3 .. $#{$row} ) {
      print OUT "<td>$row->[$j]</td>\n"
    }
    print OUT " <td class=\"View\"><a href=\"$ReportFile#EndPath\">View</a></td>\n";
    print OUT "</tr>\n";
  }
  
  print OUT "</table>\n</body></html>\n";  
  close(OUT);
  
  CopyJS($Dir);
  
  # Make sure $Dir and $BaseDir is world readable/executable.
  `chmod 755 $Dir`;
  `chmod 755 $BaseDir`;
##----------------------------------------------------------------------------##
# RunBuildCommand - Run the build command.
##----------------------------------------------------------------------------##

sub AddIfNotPresent {
  my $Args = shift;
  my $Arg = shift;  
  my $found = 0;
  
  foreach my $k (@$Args) {
    if ($k eq $Arg) {
      $found = 1;
      last;
    }
  }
  
  if ($found == 0) {
    push @$Args, $Arg;
  }
}

Ted Kremenek's avatar
Ted Kremenek committed
  my $IgnoreErrors = shift;
  if ($Cmd eq "gcc" or $Cmd eq "cc" or $Cmd eq "llvm-gcc") {
    shift @$Args;
    unshift @$Args, "ccc-analyzer"
  }
Ted Kremenek's avatar
Ted Kremenek committed
  elsif ($IgnoreErrors) {
    if ($Cmd eq "make" or $Cmd eq "gmake") {
      AddIfNotPresent($Args,"-k");
Ted Kremenek's avatar
Ted Kremenek committed
    }
    elsif ($Cmd eq "xcodebuild") {
      AddIfNotPresent($Args,"-PBXBuildsContinueAfterErrors=YES");
  } 
  
  # Disable distributed builds for xcodebuild.
  if ($Cmd eq "xcodebuild") {
    AddIfNotPresent($Args,"-nodistribute");
  }
##----------------------------------------------------------------------------##
# DisplayHelp - Utility function to display all help options.
##----------------------------------------------------------------------------##

Sam Bishop's avatar
Sam Bishop committed
sub DisplayHelp {
Sam Bishop's avatar
Sam Bishop committed
USAGE: $Prog [options] <build command> [build options]

OPTIONS:

  -o            - Target directory for HTML report files.  Subdirectories
Sam Bishop's avatar
Sam Bishop committed
                  will be created as needed to represent separate "runs" of
                  the analyzer.  If this option is not specified, a directory
                  is created in /tmp to store the reports.
                
  -h            - Display this message.
Ted Kremenek's avatar
Ted Kremenek committed
  -k            - Add a "keep on going" option to the specified build command.
Ted Kremenek's avatar
Ted Kremenek committed
  --keep-going    This option currently supports make and xcodebuild.
                  This is a convenience option; one can specify this
                  behavior directly using build options.
  -v            - Verbose output from $Prog and the analyzer.
                  A second "-v" increases verbosity.
  -V            - View analysis results in a web browser when the build
  --view          completes.

BUILD OPTIONS

  You can specify any build option acceptable to the build command.

    $Prog -o /tmp/myhtmldir make -j4
  The above example causes analysis reports to be deposited into
  a subdirectory of "/tmp/myhtmldir" and to run "make" with the "-j4" option.
  A different subdirectory is created each time $Prog analyzes a project.
  The analyzer should support most parallel builds, but not distributed builds.
}

##----------------------------------------------------------------------------##
# Process command-line arguments.
##----------------------------------------------------------------------------##

my $HtmlDir;           # Parent directory to store HTML files.
my $IgnoreErrors = 0;  # Ignore build errors.
my $ViewResults  = 0;  # View results when the build terminates.
Sam Bishop's avatar
Sam Bishop committed
  exit 1;
}

while (@ARGV) {
  
  # Scan for options we recognize.
  
  my $arg = $ARGV[0];

  if ($arg eq "-h" or $arg eq "--help") {
Sam Bishop's avatar
Sam Bishop committed
    exit 0;
      die "$Prog: '-o' option requires a target directory name.\n";
  if ($arg eq "-k" or $arg eq "--keep-going") {
    shift @ARGV;
    $IgnoreErrors = 1;
    next;
  }
  
  if ($arg eq "-v") {
    shift @ARGV;
    $Verbose++;
    next;
  }
  
  if ($arg eq "-V" or $arg eq "--view") {
    shift @ARGV;
    $ViewResults = 1;    
    next;
  }
  
  die "$Prog: unrecognized option '$arg'\n" if ($arg =~ /^-/);
  
  last;
}

if (!@ARGV) {
  print STDERR "$Prog: No build command specified.\n\n";
  DisplayHelp();
Sam Bishop's avatar
Sam Bishop committed
  exit 1;
}

# Determine the output directory for the HTML reports.

if (!defined($HtmlDir)) {
  
Sam Bishop's avatar
Sam Bishop committed
  $HtmlDir = mkdtemp("/tmp/$Prog-XXXXXX");
Sam Bishop's avatar
Sam Bishop committed
    die "error: Cannot create HTML directory in /tmp.\n";
  }
  
  if (!$Verbose) {
    print "$Prog: Using '$HtmlDir' as base HTML report directory.\n";
  }
}

Sam Bishop's avatar
Sam Bishop committed
$HtmlDir = GetHTMLRunDir($HtmlDir);

# Set the appropriate environment variables.

Sam Bishop's avatar
Sam Bishop committed
SetHtmlEnv(\@ARGV, $HtmlDir);
my $Cmd = "$RealBin/ccc-analyzer";

die "$Prog: Executable 'ccc-analyzer' does not exist at '$Cmd'\n"
  if (! -x $Cmd);
  
my $Clang = "$RealBin/clang";

if (! -x $Clang) {
  print "$Prog: 'clang' executable not found in '$RealBin'.  Using 'clang' from path.\n";
  $Clang = "clang";
}
$ENV{'CC'} = $Cmd;

if ($Verbose >= 2) {
  $ENV{'CCC_ANALYZER_VERBOSE'} = 1;
}

# Run the build.

Ted Kremenek's avatar
Ted Kremenek committed
RunBuildCommand(\@ARGV, $IgnoreErrors);

if ($ViewResults and -r "$HtmlDir/index.html") {
  # Only works on Mac OS X (for now).
  print "Viewing analysis results: '$HtmlDir/index.html'\n";
  `open $HtmlDir/index.html`
}