| #!/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 FindBin qw($RealBin); |
| use Digest::MD5; |
| use File::Basename; |
| use File::Find; |
| use File::Copy qw(copy); |
| use File::Path qw( rmtree mkpath ); |
| use Term::ANSIColor; |
| use Term::ANSIColor qw(:constants); |
| use Cwd qw/ getcwd abs_path /; |
| use Sys::Hostname; |
| use Hash::Util qw(lock_keys); |
| |
| my $Prog = "scan-build"; |
| my $BuildName; |
| my $BuildDate; |
| |
| my $TERM = $ENV{'TERM'}; |
| my $UseColor = (defined $TERM and $TERM =~ 'xterm-.*color' and -t STDOUT |
| and defined $ENV{'SCAN_BUILD_COLOR'}); |
| |
| # Portability: getpwuid is not implemented for Win32 (see Perl language |
| # reference, perlport), use getlogin instead. |
| my $UserName = HtmlEscape(getlogin() || getpwuid($<) || 'unknown'); |
| my $HostName = HtmlEscape(hostname() || 'unknown'); |
| my $CurrentDir = HtmlEscape(getcwd()); |
| |
| my $CmdArgs; |
| |
| my $Date = localtime(); |
| |
| # Command-line/config arguments. |
| my %Options = ( |
| Verbose => 0, # Verbose output from this script. |
| AnalyzeHeaders => 0, |
| OutputDir => undef, # Parent directory to store HTML files. |
| HtmlTitle => basename($CurrentDir)." - scan-build results", |
| IgnoreErrors => 0, # Ignore build errors. |
| ViewResults => 0, # View results when the build terminates. |
| ExitStatusFoundBugs => 0, # Exit status reflects whether bugs were found |
| ShowDescription => 0, # Display the description of the defect in the list |
| KeepEmpty => 0, # Don't remove output directory even with 0 results. |
| EnableCheckers => {}, |
| DisableCheckers => {}, |
| UseCC => undef, # C compiler to use for compilation. |
| UseCXX => undef, # C++ compiler to use for compilation. |
| AnalyzerTarget => undef, |
| StoreModel => undef, |
| ConstraintsModel => undef, |
| InternalStats => undef, |
| OutputFormat => "html", |
| ConfigOptions => [], # Options to pass through to the analyzer's -analyzer-config flag. |
| ReportFailures => undef, |
| AnalyzerStats => 0, |
| MaxLoop => 0, |
| PluginsToLoad => [], |
| AnalyzerDiscoveryMethod => undef, |
| OverrideCompiler => 0, # The flag corresponding to the --override-compiler command line option. |
| ForceAnalyzeDebugCode => 0 |
| ); |
| lock_keys(%Options); |
| |
| ##----------------------------------------------------------------------------## |
| # Diagnostics |
| ##----------------------------------------------------------------------------## |
| |
| sub Diag { |
| if ($UseColor) { |
| print BOLD, MAGENTA "$Prog: @_"; |
| print RESET; |
| } |
| else { |
| print "$Prog: @_"; |
| } |
| } |
| |
| sub ErrorDiag { |
| if ($UseColor) { |
| print STDERR BOLD, RED "$Prog: "; |
| print STDERR RESET, RED @_; |
| print STDERR RESET; |
| } else { |
| print STDERR "$Prog: @_"; |
| } |
| } |
| |
| sub DiagCrashes { |
| my $Dir = shift; |
| Diag ("The analyzer encountered problems on some source files.\n"); |
| Diag ("Preprocessed versions of these sources were deposited in '$Dir/failures'.\n"); |
| Diag ("Please consider submitting a bug report using these files:\n"); |
| Diag (" http://clang-analyzer.llvm.org/filing_bugs.html\n") |
| } |
| |
| sub DieDiag { |
| if ($UseColor) { |
| print STDERR BOLD, RED "$Prog: "; |
| print STDERR RESET, RED @_; |
| print STDERR RESET; |
| } |
| else { |
| print STDERR "$Prog: ", @_; |
| } |
| exit 1; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # Print default checker names |
| ##----------------------------------------------------------------------------## |
| |
| if (grep /^--help-checkers$/, @ARGV) { |
| my @options = qx($0 -h); |
| foreach (@options) { |
| next unless /^ \+/; |
| s/^\s*//; |
| my ($sign, $name, @text) = split ' ', $_; |
| print $name, $/ if $sign eq '+'; |
| } |
| exit 0; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # Declaration of Clang options. Populated later. |
| ##----------------------------------------------------------------------------## |
| |
| my $Clang; |
| my $ClangSB; |
| my $ClangCXX; |
| my $ClangVersion; |
| |
| ##----------------------------------------------------------------------------## |
| # GetHTMLRunDir - Construct an HTML directory name for the current sub-run. |
| ##----------------------------------------------------------------------------## |
| |
| sub GetHTMLRunDir { |
| die "Not enough arguments." if (@_ == 0); |
| my $Dir = shift @_; |
| my $TmpMode = 0; |
| if (!defined $Dir) { |
| $Dir = $ENV{'TMPDIR'} || $ENV{'TEMP'} || $ENV{'TMP'} || "/tmp"; |
| $TmpMode = 1; |
| } |
| |
| # Chop off any trailing '/' characters. |
| while ($Dir =~ /\/$/) { chop $Dir; } |
| |
| # Get current date and time. |
| my @CurrentTime = localtime(); |
| my $year = $CurrentTime[5] + 1900; |
| my $day = $CurrentTime[3]; |
| my $month = $CurrentTime[4] + 1; |
| my $hour = $CurrentTime[2]; |
| my $min = $CurrentTime[1]; |
| my $sec = $CurrentTime[0]; |
| |
| my $TimeString = sprintf("%02d%02d%02d", $hour, $min, $sec); |
| my $DateString = sprintf("%d-%02d-%02d-%s-$$", |
| $year, $month, $day, $TimeString); |
| |
| # Determine the run number. |
| my $RunNumber; |
| |
| if (-d $Dir) { |
| if (! -r $Dir) { |
| DieDiag("directory '$Dir' exists but is not readable.\n"); |
| } |
| # Iterate over all files in the specified directory. |
| my $max = 0; |
| opendir(DIR, $Dir); |
| my @FILES = grep { -d "$Dir/$_" } readdir(DIR); |
| closedir(DIR); |
| |
| foreach my $f (@FILES) { |
| # Strip the prefix '$Prog-' if we are dumping files to /tmp. |
| if ($TmpMode) { |
| next if (!($f =~ /^$Prog-(.+)/)); |
| $f = $1; |
| } |
| |
| my @x = split/-/, $f; |
| next if (scalar(@x) != 4); |
| next if ($x[0] != $year); |
| next if ($x[1] != $month); |
| next if ($x[2] != $day); |
| next if ($x[3] != $TimeString); |
| next if ($x[4] != $$); |
| |
| if ($x[5] > $max) { |
| $max = $x[5]; |
| } |
| } |
| |
| $RunNumber = $max + 1; |
| } |
| else { |
| |
| if (-x $Dir) { |
| DieDiag("'$Dir' exists but is not a directory.\n"); |
| } |
| |
| if ($TmpMode) { |
| DieDiag("The directory '/tmp' does not exist or cannot be accessed.\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. |
| my $NewDir; |
| if ($TmpMode) { |
| $NewDir = "$Dir/$Prog-$DateString-$RunNumber"; |
| } |
| else { |
| $NewDir = "$Dir/$DateString-$RunNumber"; |
| } |
| |
| # Make sure that the directory does not exist in order to avoid hijack. |
| if (-e $NewDir) { |
| DieDiag("The directory '$NewDir' already exists.\n"); |
| } |
| |
| mkpath($NewDir); |
| return $NewDir; |
| } |
| |
| 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/ || $Cmd =~ /autogen/) { |
| return; |
| } |
| |
| if ($Options{Verbose}) { |
| Diag("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; |
| DieDiag("Cannot read $FName to compute Digest.\n") 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 DieDiag("Cannot open $FName when computing Digest.\n"); |
| 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$//; |
| |
| if (!defined $Prefix) { |
| $Prefix = $x; |
| return; |
| } |
| |
| chop $Prefix while (!($x =~ /^\Q$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); |
| rename("$fname.tmp", $fname) |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # AddStatLine - Decode and insert a statistics line into the database. |
| ##----------------------------------------------------------------------------## |
| |
| sub AddStatLine { |
| my $Line = shift; |
| my $Stats = shift; |
| my $File = shift; |
| |
| print $Line . "\n"; |
| |
| my $Regex = qr/(.*?)\ ->\ Total\ CFGBlocks:\ (\d+)\ \|\ Unreachable |
| \ CFGBlocks:\ (\d+)\ \|\ Exhausted\ Block:\ (yes|no)\ \|\ Empty\ WorkList: |
| \ (yes|no)/x; |
| |
| if ($Line !~ $Regex) { |
| return; |
| } |
| |
| # Create a hash of the interesting fields |
| my $Row = { |
| Filename => $File, |
| Function => $1, |
| Total => $2, |
| Unreachable => $3, |
| Aborted => $4, |
| Empty => $5 |
| }; |
| |
| # Add them to the stats array |
| push @$Stats, $Row; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # 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; |
| my $Stats = 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. |
| unlink("$Dir/$FName"); |
| return; |
| } |
| |
| $AlreadyScanned{$digest} = 1; |
| |
| # At this point the report file is not world readable. Make it happen. |
| chmod(0644, "$Dir/$FName"); |
| |
| # Scan the report file for tags. |
| open(IN, "$Dir/$FName") or DieDiag("Cannot open '$Dir/$FName'\n"); |
| |
| my $BugType = ""; |
| my $BugFile = ""; |
| my $BugFunction = ""; |
| my $BugCategory = ""; |
| my $BugDescription = ""; |
| my $BugPathLength = 1; |
| my $BugLine = 0; |
| |
| while (<IN>) { |
| last if (/<!-- BUGMETAEND -->/); |
| |
| if (/<!-- BUGTYPE (.*) -->$/) { |
| $BugType = $1; |
| } |
| elsif (/<!-- BUGFILE (.*) -->$/) { |
| $BugFile = abs_path($1); |
| if (!defined $BugFile) { |
| # The file no longer exists: use the original path. |
| $BugFile = $1; |
| } |
| UpdatePrefix($BugFile); |
| } |
| elsif (/<!-- BUGPATHLENGTH (.*) -->$/) { |
| $BugPathLength = $1; |
| } |
| elsif (/<!-- BUGLINE (.*) -->$/) { |
| $BugLine = $1; |
| } |
| elsif (/<!-- BUGCATEGORY (.*) -->$/) { |
| $BugCategory = $1; |
| } |
| elsif (/<!-- BUGDESC (.*) -->$/) { |
| $BugDescription = $1; |
| } |
| elsif (/<!-- FUNCTIONNAME (.*) -->$/) { |
| $BugFunction = $1; |
| } |
| |
| } |
| |
| |
| close(IN); |
| |
| if (!defined $BugCategory) { |
| $BugCategory = "Other"; |
| } |
| |
| # Don't add internal statistics to the bug reports |
| if ($BugCategory =~ /statistics/i) { |
| AddStatLine($BugDescription, $Stats, $BugFile); |
| return; |
| } |
| |
| push @$Index,[ $FName, $BugCategory, $BugType, $BugFile, $BugFunction, $BugLine, |
| $BugPathLength ]; |
| |
| if ($Options{ShowDescription}) { |
| push @{ $Index->[-1] }, $BugDescription |
| } |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # CopyFiles - Copy resource files to target directory. |
| ##----------------------------------------------------------------------------## |
| |
| sub CopyFiles { |
| |
| my $Dir = shift; |
| |
| my $JS = Cwd::realpath("$RealBin/../share/scan-build/sorttable.js"); |
| |
| DieDiag("Cannot find 'sorttable.js'.\n") |
| if (! -r $JS); |
| |
| copy($JS, "$Dir"); |
| |
| DieDiag("Could not copy 'sorttable.js' to '$Dir'.\n") |
| if (! -r "$Dir/sorttable.js"); |
| |
| my $CSS = Cwd::realpath("$RealBin/../share/scan-build/scanview.css"); |
| |
| DieDiag("Cannot find 'scanview.css'.\n") |
| if (! -r $CSS); |
| |
| copy($CSS, "$Dir"); |
| |
| DieDiag("Could not copy 'scanview.css' to '$Dir'.\n") |
| if (! -r $CSS); |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # CalcStats - Calculates visitation statistics and returns the string. |
| ##----------------------------------------------------------------------------## |
| |
| sub CalcStats { |
| my $Stats = shift; |
| |
| my $TotalBlocks = 0; |
| my $UnreachedBlocks = 0; |
| my $TotalFunctions = scalar(@$Stats); |
| my $BlockAborted = 0; |
| my $WorkListAborted = 0; |
| my $Aborted = 0; |
| |
| # Calculate the unique files |
| my $FilesHash = {}; |
| |
| foreach my $Row (@$Stats) { |
| $FilesHash->{$Row->{Filename}} = 1; |
| $TotalBlocks += $Row->{Total}; |
| $UnreachedBlocks += $Row->{Unreachable}; |
| $BlockAborted++ if $Row->{Aborted} eq 'yes'; |
| $WorkListAborted++ if $Row->{Empty} eq 'no'; |
| $Aborted++ if $Row->{Aborted} eq 'yes' || $Row->{Empty} eq 'no'; |
| } |
| |
| my $TotalFiles = scalar(keys(%$FilesHash)); |
| |
| # Calculations |
| my $PercentAborted = sprintf("%.2f", $Aborted / $TotalFunctions * 100); |
| my $PercentBlockAborted = sprintf("%.2f", $BlockAborted / $TotalFunctions |
| * 100); |
| my $PercentWorkListAborted = sprintf("%.2f", $WorkListAborted / |
| $TotalFunctions * 100); |
| my $PercentBlocksUnreached = sprintf("%.2f", $UnreachedBlocks / $TotalBlocks |
| * 100); |
| |
| my $StatsString = "Analyzed $TotalBlocks blocks in $TotalFunctions functions" |
| . " in $TotalFiles files\n" |
| . "$Aborted functions aborted early ($PercentAborted%)\n" |
| . "$BlockAborted had aborted blocks ($PercentBlockAborted%)\n" |
| . "$WorkListAborted had unfinished worklists ($PercentWorkListAborted%)\n" |
| . "$UnreachedBlocks blocks were never reached ($PercentBlocksUnreached%)\n"; |
| |
| return $StatsString; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # Postprocess - Postprocess the results of an analysis scan. |
| ##----------------------------------------------------------------------------## |
| |
| my @filesFound; |
| my $baseDir; |
| sub FileWanted { |
| my $baseDirRegEx = quotemeta $baseDir; |
| my $file = $File::Find::name; |
| |
| # The name of the file is generated by clang binary (HTMLDiagnostics.cpp) |
| if ($file =~ /report-.*\.html$/) { |
| my $relative_file = $file; |
| $relative_file =~ s/$baseDirRegEx//g; |
| push @filesFound, $relative_file; |
| } |
| } |
| |
| sub Postprocess { |
| |
| my $Dir = shift; |
| my $BaseDir = shift; |
| my $AnalyzerStats = shift; |
| my $KeepEmpty = shift; |
| |
| die "No directory specified." if (!defined $Dir); |
| |
| if (! -d $Dir) { |
| Diag("No bugs found.\n"); |
| return 0; |
| } |
| |
| $baseDir = $Dir . "/"; |
| find({ wanted => \&FileWanted, follow => 0}, $Dir); |
| |
| if (scalar(@filesFound) == 0 and ! -e "$Dir/failures") { |
| if (! $KeepEmpty) { |
| Diag("Removing directory '$Dir' because it contains no reports.\n"); |
| rmtree($Dir) or die "Cannot rmtree '$Dir' : $!"; |
| } |
| Diag("No bugs found.\n"); |
| return 0; |
| } |
| |
| # Scan each report file and build an index. |
| my @Index; |
| my @Stats; |
| foreach my $file (@filesFound) { ScanFile(\@Index, $Dir, $file, \@Stats); } |
| |
| # Scan the failures directory and use the information in the .info files |
| # to update the common prefix directory. |
| my @failures; |
| my @attributes_ignored; |
| if (-d "$Dir/failures") { |
| opendir(DIR, "$Dir/failures"); |
| @failures = grep { /[.]info.txt$/ && !/attribute_ignored/; } readdir(DIR); |
| closedir(DIR); |
| opendir(DIR, "$Dir/failures"); |
| @attributes_ignored = grep { /^attribute_ignored/; } readdir(DIR); |
| closedir(DIR); |
| foreach my $file (@failures) { |
| open IN, "$Dir/failures/$file" or DieDiag("cannot open $file\n"); |
| my $Path = <IN>; |
| if (defined $Path) { UpdatePrefix($Path); } |
| close IN; |
| } |
| } |
| |
| # Generate an index.html file. |
| my $FName = "$Dir/index.html"; |
| open(OUT, ">", $FName) or DieDiag("Cannot create file '$FName'\n"); |
| |
| # Print out the header. |
| |
| print OUT <<ENDTEXT; |
| <html> |
| <head> |
| <title>${Options{HtmlTitle}}</title> |
| <link type="text/css" rel="stylesheet" href="scanview.css"/> |
| <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 CopyCheckedStateToCheckButtons(SummaryCheckButton) { |
| var Inputs = document.getElementsByTagName("input"); |
| for ( var i = 0 ; i < Inputs.length; ++i ) { |
| if (Inputs[i].type == "checkbox") { |
| if(Inputs[i] != SummaryCheckButton) { |
| Inputs[i].checked = SummaryCheckButton.checked; |
| Inputs[i].onclick(); |
| } |
| } |
| } |
| } |
| |
| function returnObjById( id ) { |
| if (document.getElementById) |
| var returnVar = document.getElementById(id); |
| else if (document.all) |
| var returnVar = document.all[id]; |
| else if (document.layers) |
| var returnVar = document.layers[id]; |
| return returnVar; |
| } |
| |
| var NumUnchecked = 0; |
| |
| function ToggleDisplay(CheckButton, ClassName) { |
| if (CheckButton.checked) { |
| SetDisplay(ClassName, ""); |
| if (--NumUnchecked == 0) { |
| returnObjById("AllBugsCheck").checked = true; |
| } |
| } |
| else { |
| SetDisplay(ClassName, "none"); |
| NumUnchecked++; |
| returnObjById("AllBugsCheck").checked = false; |
| } |
| } |
| </script> |
| <!-- SUMMARYENDHEAD --> |
| </head> |
| <body> |
| <h1>${Options{HtmlTitle}}</h1> |
| |
| <table> |
| <tr><th>User:</th><td>${UserName}\@${HostName}</td></tr> |
| <tr><th>Working Directory:</th><td>${CurrentDir}</td></tr> |
| <tr><th>Command Line:</th><td>${CmdArgs}</td></tr> |
| <tr><th>Clang Version:</th><td>${ClangVersion}</td></tr> |
| <tr><th>Date:</th><td>${Date}</td></tr> |
| ENDTEXT |
| |
| print OUT "<tr><th>Version:</th><td>${BuildName} (${BuildDate})</td></tr>\n" |
| if (defined($BuildName) && defined($BuildDate)); |
| |
| print OUT <<ENDTEXT; |
| </table> |
| ENDTEXT |
| |
| if (scalar(@filesFound)) { |
| # Print out the summary table. |
| my %Totals; |
| |
| for my $row ( @Index ) { |
| my $bug_type = ($row->[2]); |
| my $bug_category = ($row->[1]); |
| my $key = "$bug_category:$bug_type"; |
| |
| if (!defined $Totals{$key}) { $Totals{$key} = [1,$bug_category,$bug_type]; } |
| else { $Totals{$key}->[0]++; } |
| } |
| |
| print OUT "<h2>Bug Summary</h2>"; |
| |
| if (defined $BuildName) { |
| print OUT "\n<p>Results in this analysis run are based on analyzer build <b>$BuildName</b>.</p>\n" |
| } |
| |
| my $TotalBugs = scalar(@Index); |
| print OUT <<ENDTEXT; |
| <table> |
| <thead><tr><td>Bug Type</td><td>Quantity</td><td class="sorttable_nosort">Display?</td></tr></thead> |
| <tr style="font-weight:bold"><td class="SUMM_DESC">All Bugs</td><td class="Q">$TotalBugs</td><td><center><input type="checkbox" id="AllBugsCheck" onClick="CopyCheckedStateToCheckButtons(this);" checked/></center></td></tr> |
| ENDTEXT |
| |
| my $last_category; |
| |
| for my $key ( |
| sort { |
| my $x = $Totals{$a}; |
| my $y = $Totals{$b}; |
| my $res = $x->[1] cmp $y->[1]; |
| $res = $x->[2] cmp $y->[2] if ($res == 0); |
| $res |
| } keys %Totals ) |
| { |
| my $val = $Totals{$key}; |
| my $category = $val->[1]; |
| if (!defined $last_category or $last_category ne $category) { |
| $last_category = $category; |
| print OUT "<tr><th>$category</th><th colspan=2></th></tr>\n"; |
| } |
| my $x = lc $key; |
| $x =~ s/[ ,'":\/()]+/_/g; |
| print OUT "<tr><td class=\"SUMM_DESC\">"; |
| print OUT $val->[2]; |
| print OUT "</td><td class=\"Q\">"; |
| print OUT $val->[0]; |
| print OUT "</td><td><center><input type=\"checkbox\" onClick=\"ToggleDisplay(this,'bt_$x');\" checked/></center></td></tr>\n"; |
| } |
| |
| # Print out the table of errors. |
| |
| print OUT <<ENDTEXT; |
| </table> |
| <h2>Reports</h2> |
| |
| <table class="sortable" style="table-layout:automatic"> |
| <thead><tr> |
| <td>Bug Group</td> |
| <td class="sorttable_sorted">Bug Type<span id="sorttable_sortfwdind"> ▾</span></td> |
| <td>File</td> |
| <td>Function/Method</td> |
| <td class="Q">Line</td> |
| <td class="Q">Path Length</td> |
| ENDTEXT |
| |
| if ($Options{ShowDescription}) { |
| print OUT <<ENDTEXT; |
| <td class="Q">Description</td> |
| ENDTEXT |
| } |
| |
| print OUT <<ENDTEXT; |
| <td class="sorttable_nosort"></td> |
| <!-- REPORTBUGCOL --> |
| </tr></thead> |
| <tbody> |
| ENDTEXT |
| |
| 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->[2] cmp $b->[2] } @Index ) { |
| my $x = "$row->[1]:$row->[2]"; |
| $x = lc $x; |
| $x =~ s/[ ,'":\/()]+/_/g; |
| |
| my $ReportFile = $row->[0]; |
| |
| print OUT "<tr class=\"bt_$x\">"; |
| print OUT "<td class=\"DESC\">"; |
| print OUT $row->[1]; # $BugCategory |
| print OUT "</td>"; |
| print OUT "<td class=\"DESC\">"; |
| print OUT $row->[2]; # $BugType |
| print OUT "</td>"; |
| |
| # Update the file prefix. |
| my $fname = $row->[3]; |
| |
| if (defined $regex) { |
| $fname =~ s/$regex//; |
| UpdateInFilePath("$Dir/$ReportFile", $InFileRegex, $InFilePrefix) |
| } |
| |
| print OUT "<td>"; |
| my @fname = split /\//,$fname; |
| if ($#fname > 0) { |
| while ($#fname >= 0) { |
| my $x = shift @fname; |
| print OUT $x; |
| if ($#fname >= 0) { |
| print OUT "/"; |
| } |
| } |
| } |
| else { |
| print OUT $fname; |
| } |
| print OUT "</td>"; |
| |
| print OUT "<td class=\"DESC\">"; |
| print OUT $row->[4]; # Function |
| print OUT "</td>"; |
| |
| # Print out the quantities. |
| for my $j ( 5 .. 6 ) { # Line & Path length |
| print OUT "<td class=\"Q\">$row->[$j]</td>"; |
| } |
| |
| # Print the rest of the columns. |
| for (my $j = 7; $j <= $#{$row}; ++$j) { |
| print OUT "<td>$row->[$j]</td>" |
| } |
| |
| # Emit the "View" link. |
| print OUT "<td><a href=\"$ReportFile#EndPath\">View Report</a></td>"; |
| |
| # Emit REPORTBUG markers. |
| print OUT "\n<!-- REPORTBUG id=\"$ReportFile\" -->\n"; |
| |
| # End the row. |
| print OUT "</tr>\n"; |
| } |
| |
| print OUT "</tbody>\n</table>\n\n"; |
| } |
| |
| if (scalar (@failures) || scalar(@attributes_ignored)) { |
| print OUT "<h2>Analyzer Failures</h2>\n"; |
| |
| if (scalar @attributes_ignored) { |
| print OUT "The analyzer's parser ignored the following attributes:<p>\n"; |
| print OUT "<table>\n"; |
| print OUT "<thead><tr><td>Attribute</td><td>Source File</td><td>Preprocessed File</td><td>STDERR Output</td></tr></thead>\n"; |
| foreach my $file (sort @attributes_ignored) { |
| die "cannot demangle attribute name\n" if (! ($file =~ /^attribute_ignored_(.+).txt/)); |
| my $attribute = $1; |
| # Open the attribute file to get the first file that failed. |
| next if (!open (ATTR, "$Dir/failures/$file")); |
| my $ppfile = <ATTR>; |
| chomp $ppfile; |
| close ATTR; |
| next if (! -e "$Dir/failures/$ppfile"); |
| # Open the info file and get the name of the source file. |
| open (INFO, "$Dir/failures/$ppfile.info.txt") or |
| die "Cannot open $Dir/failures/$ppfile.info.txt\n"; |
| my $srcfile = <INFO>; |
| chomp $srcfile; |
| close (INFO); |
| # Print the information in the table. |
| my $prefix = GetPrefix(); |
| if (defined $prefix) { $srcfile =~ s/^\Q$prefix//; } |
| print OUT "<tr><td>$attribute</td><td>$srcfile</td><td><a href=\"failures/$ppfile\">$ppfile</a></td><td><a href=\"failures/$ppfile.stderr.txt\">$ppfile.stderr.txt</a></td></tr>\n"; |
| my $ppfile_clang = $ppfile; |
| $ppfile_clang =~ s/[.](.+)$/.clang.$1/; |
| print OUT " <!-- REPORTPROBLEM src=\"$srcfile\" file=\"failures/$ppfile\" clangfile=\"failures/$ppfile_clang\" stderr=\"failures/$ppfile.stderr.txt\" info=\"failures/$ppfile.info.txt\" -->\n"; |
| } |
| print OUT "</table>\n"; |
| } |
| |
| if (scalar @failures) { |
| print OUT "<p>The analyzer had problems processing the following files:</p>\n"; |
| print OUT "<table>\n"; |
| print OUT "<thead><tr><td>Problem</td><td>Source File</td><td>Preprocessed File</td><td>STDERR Output</td></tr></thead>\n"; |
| foreach my $file (sort @failures) { |
| $file =~ /(.+).info.txt$/; |
| # Get the preprocessed file. |
| my $ppfile = $1; |
| # Open the info file and get the name of the source file. |
| open (INFO, "$Dir/failures/$file") or |
| die "Cannot open $Dir/failures/$file\n"; |
| my $srcfile = <INFO>; |
| chomp $srcfile; |
| my $problem = <INFO>; |
| chomp $problem; |
| close (INFO); |
| # Print the information in the table. |
| my $prefix = GetPrefix(); |
| if (defined $prefix) { $srcfile =~ s/^\Q$prefix//; } |
| print OUT "<tr><td>$problem</td><td>$srcfile</td><td><a href=\"failures/$ppfile\">$ppfile</a></td><td><a href=\"failures/$ppfile.stderr.txt\">$ppfile.stderr.txt</a></td></tr>\n"; |
| my $ppfile_clang = $ppfile; |
| $ppfile_clang =~ s/[.](.+)$/.clang.$1/; |
| print OUT " <!-- REPORTPROBLEM src=\"$srcfile\" file=\"failures/$ppfile\" clangfile=\"failures/$ppfile_clang\" stderr=\"failures/$ppfile.stderr.txt\" info=\"failures/$ppfile.info.txt\" -->\n"; |
| } |
| print OUT "</table>\n"; |
| } |
| print OUT "<p>Please consider submitting preprocessed files as <a href=\"http://clang-analyzer.llvm.org/filing_bugs.html\">bug reports</a>. <!-- REPORTCRASHES --> </p>\n"; |
| } |
| |
| print OUT "</body></html>\n"; |
| close(OUT); |
| CopyFiles($Dir); |
| |
| # Make sure $Dir and $BaseDir are world readable/executable. |
| chmod(0755, $Dir); |
| if (defined $BaseDir) { chmod(0755, $BaseDir); } |
| |
| # Print statistics |
| print CalcStats(\@Stats) if $AnalyzerStats; |
| |
| my $Num = scalar(@Index); |
| if ($Num == 1) { |
| Diag("$Num bug found.\n"); |
| } else { |
| Diag("$Num bugs found.\n"); |
| } |
| if ($Num > 0 && -r "$Dir/index.html") { |
| Diag("Run 'scan-view $Dir' to examine bug reports.\n"); |
| } |
| |
| DiagCrashes($Dir) if (scalar @failures || scalar @attributes_ignored); |
| |
| return $Num; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # 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; |
| } |
| } |
| |
| sub SetEnv { |
| my $EnvVars = shift @_; |
| foreach my $var ('CC', 'CXX', 'CLANG', 'CLANG_CXX', |
| 'CCC_ANALYZER_ANALYSIS', 'CCC_ANALYZER_PLUGINS', |
| 'CCC_ANALYZER_CONFIG') { |
| die "$var is undefined\n" if (!defined $var); |
| $ENV{$var} = $EnvVars->{$var}; |
| } |
| foreach my $var ('CCC_ANALYZER_STORE_MODEL', |
| 'CCC_ANALYZER_CONSTRAINTS_MODEL', |
| 'CCC_ANALYZER_INTERNAL_STATS', |
| 'CCC_ANALYZER_OUTPUT_FORMAT', |
| 'CCC_CC', |
| 'CCC_CXX', |
| 'CCC_REPORT_FAILURES', |
| 'CLANG_ANALYZER_TARGET', |
| 'CCC_ANALYZER_FORCE_ANALYZE_DEBUG_CODE') { |
| my $x = $EnvVars->{$var}; |
| if (defined $x) { $ENV{$var} = $x } |
| } |
| my $Verbose = $EnvVars->{'VERBOSE'}; |
| if ($Verbose >= 2) { |
| $ENV{'CCC_ANALYZER_VERBOSE'} = 1; |
| } |
| if ($Verbose >= 3) { |
| $ENV{'CCC_ANALYZER_LOG'} = 1; |
| } |
| } |
| |
| sub RunXcodebuild { |
| my $Args = shift; |
| my $IgnoreErrors = shift; |
| my $CCAnalyzer = shift; |
| my $CXXAnalyzer = shift; |
| my $EnvVars = shift; |
| |
| if ($IgnoreErrors) { |
| AddIfNotPresent($Args,"-PBXBuildsContinueAfterErrors=YES"); |
| } |
| |
| # Detect the version of Xcode. If Xcode 4.6 or higher, use new |
| # in situ support for analyzer interposition without needed to override |
| # the compiler. |
| open(DETECT_XCODE, "-|", $Args->[0], "-version") or |
| die "error: cannot detect version of xcodebuild\n"; |
| |
| my $oldBehavior = 1; |
| |
| while(<DETECT_XCODE>) { |
| if (/^Xcode (.+)$/) { |
| my $ver = $1; |
| if ($ver =~ /^([0-9]+[.][0-9]+)[^0-9]?/) { |
| if ($1 >= 4.6) { |
| $oldBehavior = 0; |
| last; |
| } |
| } |
| } |
| } |
| close(DETECT_XCODE); |
| |
| # If --override-compiler is explicitely requested, resort to the old |
| # behavior regardless of Xcode version. |
| if ($Options{OverrideCompiler}) { |
| $oldBehavior = 1; |
| } |
| |
| if ($oldBehavior == 0) { |
| my $OutputDir = $EnvVars->{"OUTPUT_DIR"}; |
| my $CLANG = $EnvVars->{"CLANG"}; |
| my $OtherFlags = $EnvVars->{"CCC_ANALYZER_ANALYSIS"}; |
| push @$Args, |
| "RUN_CLANG_STATIC_ANALYZER=YES", |
| "CLANG_ANALYZER_OUTPUT=plist-html", |
| "CLANG_ANALYZER_EXEC=$CLANG", |
| "CLANG_ANALYZER_OUTPUT_DIR=$OutputDir", |
| "CLANG_ANALYZER_OTHER_FLAGS=$OtherFlags"; |
| |
| return (system(@$Args) >> 8); |
| } |
| |
| # Default to old behavior where we insert a bogus compiler. |
| SetEnv($EnvVars); |
| |
| # Check if using iPhone SDK 3.0 (simulator). If so the compiler being |
| # used should be gcc-4.2. |
| if (!defined $ENV{"CCC_CC"}) { |
| for (my $i = 0 ; $i < scalar(@$Args); ++$i) { |
| if ($Args->[$i] eq "-sdk" && $i + 1 < scalar(@$Args)) { |
| if (@$Args[$i+1] =~ /^iphonesimulator3/) { |
| $ENV{"CCC_CC"} = "gcc-4.2"; |
| $ENV{"CCC_CXX"} = "g++-4.2"; |
| } |
| } |
| } |
| } |
| |
| # Disable PCH files until clang supports them. |
| AddIfNotPresent($Args,"GCC_PRECOMPILE_PREFIX_HEADER=NO"); |
| |
| # When 'CC' is set, xcodebuild uses it to do all linking, even if we are |
| # linking C++ object files. Set 'LDPLUSPLUS' so that xcodebuild uses 'g++' |
| # (via c++-analyzer) when linking such files. |
| $ENV{"LDPLUSPLUS"} = $CXXAnalyzer; |
| |
| return (system(@$Args) >> 8); |
| } |
| |
| sub RunBuildCommand { |
| my $Args = shift; |
| my $IgnoreErrors = shift; |
| my $Cmd = $Args->[0]; |
| my $CCAnalyzer = shift; |
| my $CXXAnalyzer = shift; |
| my $EnvVars = shift; |
| |
| if ($Cmd =~ /\bxcodebuild$/) { |
| return RunXcodebuild($Args, $IgnoreErrors, $CCAnalyzer, $CXXAnalyzer, $EnvVars); |
| } |
| |
| # Setup the environment. |
| SetEnv($EnvVars); |
| |
| if ($Cmd =~ /(.*\/?gcc[^\/]*$)/ or |
| $Cmd =~ /(.*\/?cc[^\/]*$)/ or |
| $Cmd =~ /(.*\/?llvm-gcc[^\/]*$)/ or |
| $Cmd =~ /(.*\/?clang$)/ or |
| $Cmd =~ /(.*\/?ccc-analyzer[^\/]*$)/) { |
| |
| if (!($Cmd =~ /ccc-analyzer/) and !defined $ENV{"CCC_CC"}) { |
| $ENV{"CCC_CC"} = $1; |
| } |
| |
| shift @$Args; |
| unshift @$Args, $CCAnalyzer; |
| } |
| elsif ($Cmd =~ /(.*\/?g\+\+[^\/]*$)/ or |
| $Cmd =~ /(.*\/?c\+\+[^\/]*$)/ or |
| $Cmd =~ /(.*\/?llvm-g\+\+[^\/]*$)/ or |
| $Cmd =~ /(.*\/?clang\+\+$)/ or |
| $Cmd =~ /(.*\/?c\+\+-analyzer[^\/]*$)/) { |
| if (!($Cmd =~ /c\+\+-analyzer/) and !defined $ENV{"CCC_CXX"}) { |
| $ENV{"CCC_CXX"} = $1; |
| } |
| shift @$Args; |
| unshift @$Args, $CXXAnalyzer; |
| } |
| elsif ($Cmd eq "make" or $Cmd eq "gmake" or $Cmd eq "mingw32-make") { |
| AddIfNotPresent($Args, "CC=$CCAnalyzer"); |
| AddIfNotPresent($Args, "CXX=$CXXAnalyzer"); |
| if ($IgnoreErrors) { |
| AddIfNotPresent($Args,"-k"); |
| AddIfNotPresent($Args,"-i"); |
| } |
| } |
| |
| return (system(@$Args) >> 8); |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # DisplayHelp - Utility function to display all help options. |
| ##----------------------------------------------------------------------------## |
| |
| sub DisplayHelp { |
| |
| my $ArgClangNotFoundErrMsg = shift; |
| print <<ENDTEXT; |
| USAGE: $Prog [options] <build command> [build options] |
| |
| ENDTEXT |
| |
| if (defined $BuildName) { |
| print "ANALYZER BUILD: $BuildName ($BuildDate)\n\n"; |
| } |
| |
| print <<ENDTEXT; |
| OPTIONS: |
| |
| -analyze-headers |
| |
| Also analyze functions in #included files. By default, such functions |
| are skipped unless they are called by functions within the main source file. |
| |
| --force-analyze-debug-code |
| |
| Tells analyzer to enable assertions in code even if they were disabled |
| during compilation to enable more precise results. |
| |
| -o <output location> |
| |
| Specifies the output directory for analyzer reports. Subdirectories will be |
| created as needed to represent separate "runs" of the analyzer. If this |
| option is not specified, a directory is created in /tmp (TMPDIR on Mac OS X) |
| to store the reports. |
| |
| -h |
| --help |
| |
| Display this message. |
| |
| -k |
| --keep-going |
| |
| Add a "keep on going" option to the specified build command. This option |
| currently supports make and xcodebuild. This is a convenience option; one |
| can specify this behavior directly using build options. |
| |
| --html-title [title] |
| --html-title=[title] |
| |
| Specify the title used on generated HTML pages. If not specified, a default |
| title will be used. |
| |
| --show-description |
| |
| Display the description of defects in the list |
| |
| -plist |
| |
| By default the output of scan-build is a set of HTML files. This option |
| outputs the results as a set of .plist files. |
| |
| -plist-html |
| |
| By default the output of scan-build is a set of HTML files. This option |
| outputs the results as a set of HTML and .plist files. |
| |
| --status-bugs |
| |
| By default, the exit status of scan-build is the same as the executed build |
| command. Specifying this option causes the exit status of scan-build to be 1 |
| if it found potential bugs and 0 otherwise. |
| |
| --use-cc [compiler path] |
| --use-cc=[compiler path] |
| |
| scan-build analyzes a project by interposing a "fake compiler", which |
| executes a real compiler for compilation and the static analyzer for analysis. |
| Because of the current implementation of interposition, scan-build does not |
| know what compiler your project normally uses. Instead, it simply overrides |
| the CC environment variable, and guesses your default compiler. |
| |
| In the future, this interposition mechanism to be improved, but if you need |
| scan-build to use a specific compiler for *compilation* then you can use |
| this option to specify a path to that compiler. |
| |
| If the given compiler is a cross compiler, you may also need to provide |
| --analyzer-target option to properly analyze the source code because static |
| analyzer runs as if the code is compiled for the host machine by default. |
| |
| --use-c++ [compiler path] |
| --use-c++=[compiler path] |
| |
| This is the same as "--use-cc" but for C++ code. |
| |
| --analyzer-target [target triple name for analysis] |
| --analyzer-target=[target triple name for analysis] |
| |
| This provides target triple information to clang static analyzer. |
| It only changes the target for analysis but doesn't change the target of a |
| real compiler given by --use-cc and --use-c++ options. |
| |
| -v |
| |
| Enable verbose output from scan-build. A second and third '-v' increases |
| verbosity. |
| |
| -V |
| --view |
| |
| View analysis results in a web browser when the build completes. |
| |
| ADVANCED OPTIONS: |
| |
| -no-failure-reports |
| |
| Do not create a 'failures' subdirectory that includes analyzer crash reports |
| and preprocessed source files. |
| |
| -stats |
| |
| Generates visitation statistics for the project being analyzed. |
| |
| -maxloop <loop count> |
| |
| Specifiy the number of times a block can be visited before giving up. |
| Default is 4. Increase for more comprehensive coverage at a cost of speed. |
| |
| -internal-stats |
| |
| Generate internal analyzer statistics. |
| |
| --use-analyzer [Xcode|path to clang] |
| --use-analyzer=[Xcode|path to clang] |
| |
| scan-build uses the 'clang' executable relative to itself for static |
| analysis. One can override this behavior with this option by using the |
| 'clang' packaged with Xcode (on OS X) or from the PATH. |
| |
| --keep-empty |
| |
| Don't remove the build results directory even if no issues were reported. |
| |
| --override-compiler |
| Always resort to the ccc-analyzer even when better interposition methods |
| are available. |
| |
| -analyzer-config <options> |
| |
| Provide options to pass through to the analyzer's -analyzer-config flag. |
| Several options are separated with comma: 'key1=val1,key2=val2' |
| |
| Available options: |
| * stable-report-filename=true or false (default) |
| Switch the page naming to: |
| report-<filename>-<function/method name>-<id>.html |
| instead of report-XXXXXX.html |
| |
| CONTROLLING CHECKERS: |
| |
| A default group of checkers are always run unless explicitly disabled. |
| Checkers may be enabled/disabled using the following options: |
| |
| -enable-checker [checker name] |
| -disable-checker [checker name] |
| |
| LOADING CHECKERS: |
| |
| Loading external checkers using the clang plugin interface: |
| |
| -load-plugin [plugin library] |
| ENDTEXT |
| |
| if (defined $Clang && -x $Clang) { |
| # Query clang for list of checkers that are enabled. |
| |
| # create a list to load the plugins via the 'Xclang' command line |
| # argument |
| my @PluginLoadCommandline_xclang; |
| foreach my $param ( @{$Options{PluginsToLoad}} ) { |
| push ( @PluginLoadCommandline_xclang, "-Xclang" ); |
| push ( @PluginLoadCommandline_xclang, "-load" ); |
| push ( @PluginLoadCommandline_xclang, "-Xclang" ); |
| push ( @PluginLoadCommandline_xclang, $param ); |
| } |
| |
| my %EnabledCheckers; |
| foreach my $lang ("c", "objective-c", "objective-c++", "c++") { |
| my $ExecLine = join(' ', qq/"$Clang"/, @PluginLoadCommandline_xclang, "--analyze", "-x", $lang, "-", "-###", "2>&1", "|"); |
| open(PS, $ExecLine); |
| while (<PS>) { |
| foreach my $val (split /\s+/) { |
| $val =~ s/\"//g; |
| if ($val =~ /-analyzer-checker\=([^\s]+)/) { |
| $EnabledCheckers{$1} = 1; |
| } |
| } |
| } |
| } |
| |
| # Query clang for complete list of checkers. |
| my @PluginLoadCommandline; |
| foreach my $param ( @{$Options{PluginsToLoad}} ) { |
| push ( @PluginLoadCommandline, "-load" ); |
| push ( @PluginLoadCommandline, $param ); |
| } |
| |
| my $ExecLine = join(' ', qq/"$Clang"/, "-cc1", @PluginLoadCommandline, "-analyzer-checker-help", "2>&1", "|"); |
| open(PS, $ExecLine); |
| my $foundCheckers = 0; |
| while (<PS>) { |
| if (/CHECKERS:/) { |
| $foundCheckers = 1; |
| last; |
| } |
| } |
| if (!$foundCheckers) { |
| print " *** Could not query Clang for the list of available checkers."; |
| } |
| else { |
| print("\nAVAILABLE CHECKERS:\n\n"); |
| my $skip = 0; |
| while(<PS>) { |
| if (/experimental/) { |
| $skip = 1; |
| next; |
| } |
| if ($skip) { |
| next if (!/^\s\s[^\s]/); |
| $skip = 0; |
| } |
| s/^\s\s//; |
| if (/^([^\s]+)/) { |
| # Is the checker enabled? |
| my $checker = $1; |
| my $enabled = 0; |
| my $aggregate = ""; |
| foreach my $domain (split /\./, $checker) { |
| $aggregate .= $domain; |
| if ($EnabledCheckers{$aggregate}) { |
| $enabled =1; |
| last; |
| } |
| # append a dot, if an additional domain is added in the next iteration |
| $aggregate .= "."; |
| } |
| |
| if ($enabled) { |
| print " + "; |
| } |
| else { |
| print " "; |
| } |
| } |
| else { |
| print " "; |
| } |
| print $_; |
| } |
| print "\nNOTE: \"+\" indicates that an analysis is enabled by default.\n"; |
| } |
| close PS; |
| } |
| else { |
| print " *** Could not query Clang for the list of available checkers.\n"; |
| if (defined $ArgClangNotFoundErrMsg) { |
| print " *** Reason: $ArgClangNotFoundErrMsg\n"; |
| } |
| } |
| |
| print <<ENDTEXT |
| |
| BUILD OPTIONS |
| |
| You can specify any build option acceptable to the build command. |
| |
| EXAMPLE |
| |
| scan-build -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 scan-build analyzes a project. The analyzer |
| should support most parallel builds, but not distributed builds. |
| |
| ENDTEXT |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # HtmlEscape - HTML entity encode characters that are special in HTML |
| ##----------------------------------------------------------------------------## |
| |
| sub HtmlEscape { |
| # copy argument to new variable so we don't clobber the original |
| my $arg = shift || ''; |
| my $tmp = $arg; |
| $tmp =~ s/&/&/g; |
| $tmp =~ s/</</g; |
| $tmp =~ s/>/>/g; |
| return $tmp; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # ShellEscape - backslash escape characters that are special to the shell |
| ##----------------------------------------------------------------------------## |
| |
| sub ShellEscape { |
| # copy argument to new variable so we don't clobber the original |
| my $arg = shift || ''; |
| if ($arg =~ /["\s]/) { return "'" . $arg . "'"; } |
| return $arg; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # FindClang - searches for 'clang' executable. |
| ##----------------------------------------------------------------------------## |
| |
| sub FindClang { |
| if (!defined $Options{AnalyzerDiscoveryMethod}) { |
| $Clang = Cwd::realpath("$RealBin/bin/clang") if (-f "$RealBin/bin/clang"); |
| if (!defined $Clang || ! -x $Clang) { |
| $Clang = Cwd::realpath("$RealBin/clang") if (-f "$RealBin/clang"); |
| } |
| if (!defined $Clang || ! -x $Clang) { |
| return "error: Cannot find an executable 'clang' relative to" . |
| " scan-build. Consider using --use-analyzer to pick a version of" . |
| " 'clang' to use for static analysis.\n"; |
| } |
| } |
| else { |
| if ($Options{AnalyzerDiscoveryMethod} =~ /^[Xx]code$/) { |
| my $xcrun = `which xcrun`; |
| chomp $xcrun; |
| if ($xcrun eq "") { |
| return "Cannot find 'xcrun' to find 'clang' for analysis.\n"; |
| } |
| $Clang = `$xcrun -toolchain XcodeDefault -find clang`; |
| chomp $Clang; |
| if ($Clang eq "") { |
| return "No 'clang' executable found by 'xcrun'\n"; |
| } |
| } |
| else { |
| $Clang = $Options{AnalyzerDiscoveryMethod}; |
| if (!defined $Clang or not -x $Clang) { |
| return "Cannot find an executable clang at '$Options{AnalyzerDiscoveryMethod}'\n"; |
| } |
| } |
| } |
| return undef; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # Process command-line arguments. |
| ##----------------------------------------------------------------------------## |
| |
| my $RequestDisplayHelp = 0; |
| my $ForceDisplayHelp = 0; |
| |
| sub ProcessArgs { |
| my $Args = shift; |
| my $NumArgs = 0; |
| |
| while (@$Args) { |
| |
| $NumArgs++; |
| |
| # Scan for options we recognize. |
| |
| my $arg = $Args->[0]; |
| |
| if ($arg eq "-h" or $arg eq "--help") { |
| $RequestDisplayHelp = 1; |
| shift @$Args; |
| next; |
| } |
| |
| if ($arg eq '-analyze-headers') { |
| shift @$Args; |
| $Options{AnalyzeHeaders} = 1; |
| next; |
| } |
| |
| if ($arg eq "-o") { |
| shift @$Args; |
| |
| if (!@$Args) { |
| DieDiag("'-o' option requires a target directory name.\n"); |
| } |
| |
| # Construct an absolute path. Uses the current working directory |
| # as a base if the original path was not absolute. |
| my $OutDir = shift @$Args; |
| mkpath($OutDir) unless (-e $OutDir); # abs_path wants existing dir |
| $Options{OutputDir} = abs_path($OutDir); |
| |
| next; |
| } |
| |
| if ($arg =~ /^--html-title(=(.+))?$/) { |
| shift @$Args; |
| |
| if (!defined $2 || $2 eq '') { |
| if (!@$Args) { |
| DieDiag("'--html-title' option requires a string.\n"); |
| } |
| |
| $Options{HtmlTitle} = shift @$Args; |
| } else { |
| $Options{HtmlTitle} = $2; |
| } |
| |
| next; |
| } |
| |
| if ($arg eq "-k" or $arg eq "--keep-going") { |
| shift @$Args; |
| $Options{IgnoreErrors} = 1; |
| next; |
| } |
| |
| if ($arg =~ /^--use-cc(=(.+))?$/) { |
| shift @$Args; |
| my $cc; |
| |
| if (!defined $2 || $2 eq "") { |
| if (!@$Args) { |
| DieDiag("'--use-cc' option requires a compiler executable name.\n"); |
| } |
| $cc = shift @$Args; |
| } |
| else { |
| $cc = $2; |
| } |
| |
| $Options{UseCC} = $cc; |
| next; |
| } |
| |
| if ($arg =~ /^--use-c\+\+(=(.+))?$/) { |
| shift @$Args; |
| my $cxx; |
| |
| if (!defined $2 || $2 eq "") { |
| if (!@$Args) { |
| DieDiag("'--use-c++' option requires a compiler executable name.\n"); |
| } |
| $cxx = shift @$Args; |
| } |
| else { |
| $cxx = $2; |
| } |
| |
| $Options{UseCXX} = $cxx; |
| next; |
| } |
| |
| if ($arg =~ /^--analyzer-target(=(.+))?$/) { |
| shift @ARGV; |
| my $AnalyzerTarget; |
| |
| if (!defined $2 || $2 eq "") { |
| if (!@ARGV) { |
| DieDiag("'--analyzer-target' option requires a target triple name.\n"); |
| } |
| $AnalyzerTarget = shift @ARGV; |
| } |
| else { |
| $AnalyzerTarget = $2; |
| } |
| |
| $Options{AnalyzerTarget} = $AnalyzerTarget; |
| next; |
| } |
| |
| if ($arg eq "-v") { |
| shift @$Args; |
| $Options{Verbose}++; |
| next; |
| } |
| |
| if ($arg eq "-V" or $arg eq "--view") { |
| shift @$Args; |
| $Options{ViewResults} = 1; |
| next; |
| } |
| |
| if ($arg eq "--status-bugs") { |
| shift @$Args; |
| $Options{ExitStatusFoundBugs} = 1; |
| next; |
| } |
| |
| if ($arg eq "--show-description") { |
| shift @$Args; |
| $Options{ShowDescription} = 1; |
| next; |
| } |
| |
| if ($arg eq "-store") { |
| shift @$Args; |
| $Options{StoreModel} = shift @$Args; |
| next; |
| } |
| |
| if ($arg eq "-constraints") { |
| shift @$Args; |
| $Options{ConstraintsModel} = shift @$Args; |
| next; |
| } |
| |
| if ($arg eq "-internal-stats") { |
| shift @$Args; |
| $Options{InternalStats} = 1; |
| next; |
| } |
| |
| if ($arg eq "-plist") { |
| shift @$Args; |
| $Options{OutputFormat} = "plist"; |
| next; |
| } |
| |
| if ($arg eq "-plist-html") { |
| shift @$Args; |
| $Options{OutputFormat} = "plist-html"; |
| next; |
| } |
| |
| if ($arg eq "-analyzer-config") { |
| shift @$Args; |
| push @{$Options{ConfigOptions}}, shift @$Args; |
| next; |
| } |
| |
| if ($arg eq "-no-failure-reports") { |
| shift @$Args; |
| $Options{ReportFailures} = 0; |
| next; |
| } |
| |
| if ($arg eq "-stats") { |
| shift @$Args; |
| $Options{AnalyzerStats} = 1; |
| next; |
| } |
| |
| if ($arg eq "-maxloop") { |
| shift @$Args; |
| $Options{MaxLoop} = shift @$Args; |
| next; |
| } |
| |
| if ($arg eq "-enable-checker") { |
| shift @$Args; |
| my $Checker = shift @$Args; |
| # Store $NumArgs to preserve the order the checkers were enabled. |
| $Options{EnableCheckers}{$Checker} = $NumArgs; |
| delete $Options{DisableCheckers}{$Checker}; |
| next; |
| } |
| |
| if ($arg eq "-disable-checker") { |
| shift @$Args; |
| my $Checker = shift @$Args; |
| # Store $NumArgs to preserve the order the checkers were disabled. |
| $Options{DisableCheckers}{$Checker} = $NumArgs; |
| delete $Options{EnableCheckers}{$Checker}; |
| next; |
| } |
| |
| if ($arg eq "-load-plugin") { |
| shift @$Args; |
| push @{$Options{PluginsToLoad}}, shift @$Args; |
| next; |
| } |
| |
| if ($arg eq "--use-analyzer") { |
| shift @$Args; |
| $Options{AnalyzerDiscoveryMethod} = shift @$Args; |
| next; |
| } |
| |
| if ($arg =~ /^--use-analyzer=(.+)$/) { |
| shift @$Args; |
| $Options{AnalyzerDiscoveryMethod} = $1; |
| next; |
| } |
| |
| if ($arg eq "--keep-empty") { |
| shift @$Args; |
| $Options{KeepEmpty} = 1; |
| next; |
| } |
| |
| if ($arg eq "--override-compiler") { |
| shift @$Args; |
| $Options{OverrideCompiler} = 1; |
| next; |
| } |
| |
| if ($arg eq "--force-analyze-debug-code") { |
| shift @$Args; |
| $Options{ForceAnalyzeDebugCode} = 1; |
| next; |
| } |
| |
| DieDiag("unrecognized option '$arg'\n") if ($arg =~ /^-/); |
| |
| $NumArgs--; |
| last; |
| } |
| return $NumArgs; |
| } |
| |
| if (!@ARGV) { |
| $ForceDisplayHelp = 1 |
| } |
| |
| ProcessArgs(\@ARGV); |
| # All arguments are now shifted from @ARGV. The rest is a build command, if any. |
| |
| if (!@ARGV and !$RequestDisplayHelp) { |
| ErrorDiag("No build command specified.\n\n"); |
| $ForceDisplayHelp = 1; |
| } |
| |
| my $ClangNotFoundErrMsg = FindClang(); |
| |
| if ($ForceDisplayHelp || $RequestDisplayHelp) { |
| DisplayHelp($ClangNotFoundErrMsg); |
| exit $ForceDisplayHelp; |
| } |
| |
| DieDiag($ClangNotFoundErrMsg) if (defined $ClangNotFoundErrMsg); |
| |
| $ClangCXX = $Clang; |
| if ($Clang !~ /\+\+(\.exe)?$/) { |
| # If $Clang holds the name of the clang++ executable then we leave |
| # $ClangCXX and $Clang equal, otherwise construct the name of the clang++ |
| # executable from the clang executable name. |
| |
| # Determine operating system under which this copy of Perl was built. |
| my $IsWinBuild = ($^O =~/msys|cygwin|MSWin32/); |
| if($IsWinBuild) { |
| $ClangCXX =~ s/.exe$/++.exe/; |
| } |
| else { |
| $ClangCXX =~ s/\-\d+\.\d+$//; |
| $ClangCXX .= "++"; |
| } |
| } |
| |
| # Make sure to use "" to handle paths with spaces. |
| $ClangVersion = HtmlEscape(`"$Clang" --version`); |
| |
| # Determine where results go. |
| $CmdArgs = HtmlEscape(join(' ', map(ShellEscape($_), @ARGV))); |
| |
| # Determine the output directory for the HTML reports. |
| my $BaseDir = $Options{OutputDir}; |
| $Options{OutputDir} = GetHTMLRunDir($Options{OutputDir}); |
| |
| # Determine the location of ccc-analyzer. |
| my $AbsRealBin = Cwd::realpath($RealBin); |
| my $Cmd = "$AbsRealBin/../libexec/ccc-analyzer"; |
| my $CmdCXX = "$AbsRealBin/../libexec/c++-analyzer"; |
| |
| # Portability: use less strict but portable check -e (file exists) instead of |
| # non-portable -x (file is executable). On some windows ports -x just checks |
| # file extension to determine if a file is executable (see Perl language |
| # reference, perlport) |
| if (!defined $Cmd || ! -e $Cmd) { |
| $Cmd = "$AbsRealBin/ccc-analyzer"; |
| DieDiag("'ccc-analyzer' does not exist at '$Cmd'\n") if(! -e $Cmd); |
| } |
| if (!defined $CmdCXX || ! -e $CmdCXX) { |
| $CmdCXX = "$AbsRealBin/c++-analyzer"; |
| DieDiag("'c++-analyzer' does not exist at '$CmdCXX'\n") if(! -e $CmdCXX); |
| } |
| |
| Diag("Using '$Clang' for static analysis\n"); |
| |
| SetHtmlEnv(\@ARGV, $Options{OutputDir}); |
| |
| my @AnalysesToRun; |
| foreach (sort { $Options{EnableCheckers}{$a} <=> $Options{EnableCheckers}{$b} } |
| keys %{$Options{EnableCheckers}}) { |
| # Push checkers in order they were enabled. |
| push @AnalysesToRun, "-analyzer-checker", $_; |
| } |
| foreach (sort { $Options{DisableCheckers}{$a} <=> $Options{DisableCheckers}{$b} } |
| keys %{$Options{DisableCheckers}}) { |
| # Push checkers in order they were disabled. |
| push @AnalysesToRun, "-analyzer-disable-checker", $_; |
| } |
| if ($Options{AnalyzeHeaders}) { push @AnalysesToRun, "-analyzer-opt-analyze-headers"; } |
| if ($Options{AnalyzerStats}) { push @AnalysesToRun, '-analyzer-checker=debug.Stats'; } |
| if ($Options{MaxLoop} > 0) { push @AnalysesToRun, "-analyzer-max-loop $Options{MaxLoop}"; } |
| |
| # Delay setting up other environment variables in case we can do true |
| # interposition. |
| my $CCC_ANALYZER_ANALYSIS = join ' ', @AnalysesToRun; |
| my $CCC_ANALYZER_PLUGINS = join ' ', map { "-load ".$_ } @{$Options{PluginsToLoad}}; |
| my $CCC_ANALYZER_CONFIG = join ' ', map { "-analyzer-config ".$_ } @{$Options{ConfigOptions}}; |
| my %EnvVars = ( |
| 'CC' => $Cmd, |
| 'CXX' => $CmdCXX, |
| 'CLANG' => $Clang, |
| 'CLANG_CXX' => $ClangCXX, |
| 'VERBOSE' => $Options{Verbose}, |
| 'CCC_ANALYZER_ANALYSIS' => $CCC_ANALYZER_ANALYSIS, |
| 'CCC_ANALYZER_PLUGINS' => $CCC_ANALYZER_PLUGINS, |
| 'CCC_ANALYZER_CONFIG' => $CCC_ANALYZER_CONFIG, |
| 'OUTPUT_DIR' => $Options{OutputDir}, |
| 'CCC_CC' => $Options{UseCC}, |
| 'CCC_CXX' => $Options{UseCXX}, |
| 'CCC_REPORT_FAILURES' => $Options{ReportFailures}, |
| 'CCC_ANALYZER_STORE_MODEL' => $Options{StoreModel}, |
| 'CCC_ANALYZER_CONSTRAINTS_MODEL' => $Options{ConstraintsModel}, |
| 'CCC_ANALYZER_INTERNAL_STATS' => $Options{InternalStats}, |
| 'CCC_ANALYZER_OUTPUT_FORMAT' => $Options{OutputFormat}, |
| 'CLANG_ANALYZER_TARGET' => $Options{AnalyzerTarget}, |
| 'CCC_ANALYZER_FORCE_ANALYZE_DEBUG_CODE' => $Options{ForceAnalyzeDebugCode} |
| ); |
| |
| # Run the build. |
| my $ExitStatus = RunBuildCommand(\@ARGV, $Options{IgnoreErrors}, $Cmd, $CmdCXX, |
| \%EnvVars); |
| |
| if (defined $Options{OutputFormat}) { |
| if ($Options{OutputFormat} =~ /plist/) { |
| Diag "Analysis run complete.\n"; |
| Diag "Analysis results (plist files) deposited in '$Options{OutputDir}'\n"; |
| } |
| if ($Options{OutputFormat} =~ /html/) { |
| # Postprocess the HTML directory. |
| my $NumBugs = Postprocess($Options{OutputDir}, $BaseDir, |
| $Options{AnalyzerStats}, $Options{KeepEmpty}); |
| |
| if ($Options{ViewResults} and -r "$Options{OutputDir}/index.html") { |
| Diag "Analysis run complete.\n"; |
| Diag "Viewing analysis results in '$Options{OutputDir}' using scan-view.\n"; |
| my $ScanView = Cwd::realpath("$RealBin/scan-view"); |
| if (! -x $ScanView) { $ScanView = "scan-view"; } |
| if (! -x $ScanView) { $ScanView = Cwd::realpath("$RealBin/../../scan-view/bin/scan-view"); } |
| exec $ScanView, "$Options{OutputDir}"; |
| } |
| |
| if ($Options{ExitStatusFoundBugs}) { |
| exit 1 if ($NumBugs > 0); |
| exit 0; |
| } |
| } |
| } |
| |
| exit $ExitStatus; |