import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,372 @@
#!/usr/bin/perl -w
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
################################################################################
sub usage() {
print <<EOUSAGE;
# bloatdiff.pl - munges the output from
# XPCOM_MEM_BLOAT_LOG=1
# firefox-bin -P default resource:///res/bloatcycle.html
# so that it does some summary and stats stuff.
#
# To show leak test results for a set of changes, do something like this:
#
# XPCOM_MEM_BLOAT_LOG=1
# firefox-bin -P default resource:///res/bloatcycle.html > a.out
# **make change**
# firefox-bin -P default resource:///res/bloatcycle.html > b.out
# bloatdiff.pl a.out b.out
EOUSAGE
}
$OLDFILE = $ARGV[0];
$NEWFILE = $ARGV[1];
#$LABEL = $ARGV[2];
if (!$OLDFILE or
! -e $OLDFILE or
-z $OLDFILE) {
print "\nError: Previous log file not specified, does not exist, or is empty.\n\n";
&usage();
exit 1;
}
if (!$NEWFILE or
! -e $NEWFILE or
-z $NEWFILE) {
print "\nError: Current log file not specified, does not exist, or is empty.\n\n";
&usage();
exit 1;
}
sub processFile {
my ($filename, $map, $prevMap) = @_;
open(FH, $filename);
while (<FH>) {
if (m{
^\s*(\d+)\s # Line number
([\w:]+)\s+ # Name
(-?\d+)\s+ # Size
(-?\d+)\s+ # Leaked
(-?\d+)\s+ # Objects Total
(-?\d+)\s+ # Objects Rem
\(\s*(-?[\d.]+)\s+ # Objects Mean
\+/-\s+
([\w.]+)\)\s+ # Objects StdDev
(-?\d+)\s+ # Reference Total
(-?\d+)\s+ # Reference Rem
\(\s*(-?[\d.]+)\s+ # Reference Mean
\+/-\s+
([\w\.]+)\) # Reference StdDev
}x) {
$$map{$2} = { name => $2,
size => $3,
leaked => $4,
objTotal => $5,
objRem => $6,
objMean => $7,
objStdDev => $8,
refTotal => $9,
refRem => $10,
refMean => $11,
refStdDev => $12,
bloat => $3 * $5 # size * objTotal
};
} else {
# print "failed to parse: $_\n";
}
}
close(FH);
}
%oldMap = ();
processFile($OLDFILE, \%oldMap);
%newMap = ();
processFile($NEWFILE, \%newMap);
################################################################################
$inf = 9999999.99;
sub getLeaksDelta {
my ($key) = @_;
my $oldLeaks = $oldMap{$key}{leaked} || 0;
my $newLeaks = $newMap{$key}{leaked};
my $percentLeaks = 0;
if ($oldLeaks == 0) {
if ($newLeaks != 0) {
# there weren't any leaks before, but now there are!
$percentLeaks = $inf;
}
}
else {
$percentLeaks = ($newLeaks - $oldLeaks) / $oldLeaks * 100;
}
# else we had no record of this class before
return ($newLeaks - $oldLeaks, $percentLeaks);
}
################################################################################
sub getBloatDelta {
my ($key) = @_;
my $newBloat = $newMap{$key}{bloat};
my $percentBloat = 0;
my $oldSize = $oldMap{$key}{size} || 0;
my $oldTotal = $oldMap{$key}{objTotal} || 0;
my $oldBloat = $oldTotal * $oldSize;
if ($oldBloat == 0) {
if ($newBloat != 0) {
# this class wasn't used before, but now it is
$percentBloat = $inf;
}
}
else {
$percentBloat = ($newBloat - $oldBloat) / $oldBloat * 100;
}
# else we had no record of this class before
return ($newBloat - $oldBloat, $percentBloat);
}
################################################################################
foreach $key (keys %newMap) {
my ($newLeaks, $percentLeaks) = getLeaksDelta($key);
my ($newBloat, $percentBloat) = getBloatDelta($key);
$newMap{$key}{leakDelta} = $newLeaks;
$newMap{$key}{leakPercent} = $percentLeaks;
$newMap{$key}{bloatDelta} = $newBloat;
$newMap{$key}{bloatPercent} = $percentBloat;
}
################################################################################
# Print a value of bytes out in a reasonable
# KB, MB, or GB form. Copied from build-seamonkey-util.pl, sorry. -mcafee
sub PrintSize($) {
# print a number with 3 significant figures
sub PrintNum($) {
my ($num) = @_;
my $rv;
if ($num < 1) {
$rv = sprintf "%.3f", ($num);
} elsif ($num < 10) {
$rv = sprintf "%.2f", ($num);
} elsif ($num < 100) {
$rv = sprintf "%.1f", ($num);
} else {
$rv = sprintf "%d", ($num);
}
}
my ($size) = @_;
my $rv;
if ($size > 1000000000) {
$rv = PrintNum($size / 1000000000.0) . "G";
} elsif ($size > 1000000) {
$rv = PrintNum($size / 1000000.0) . "M";
} elsif ($size > 1000) {
$rv = PrintNum($size / 1000.0) . "K";
} else {
$rv = PrintNum($size);
}
}
print "Bloat/Leak Delta Report\n";
print "--------------------------------------------------------------------------------------\n";
print "Current file: $NEWFILE\n";
print "Previous file: $OLDFILE\n";
print "----------------------------------------------leaks------leaks%------bloat------bloat%\n";
if (! $newMap{"TOTAL"} or
! $newMap{"TOTAL"}{bloat}) {
# It's OK if leaked or leakPercent are 0 (in fact, that would be good).
# If bloatPercent is zero, it is also OK, because we may have just had
# two runs exactly the same or with no new bloat.
print "\nError: unable to calculate bloat/leak data.\n";
print "There is no data present.\n\n";
print "HINT - Did your test run complete successfully?\n";
print "HINT - Are you pointing at the right log files?\n\n";
&usage();
exit 1;
}
printf "%-40s %10s %10.2f%% %10s %10.2f%%\n",
("TOTAL",
$newMap{"TOTAL"}{leaked}, $newMap{"TOTAL"}{leakPercent},
$newMap{"TOTAL"}{bloat}, $newMap{"TOTAL"}{bloatPercent});
################################################################################
sub percentStr {
my ($p) = @_;
if ($p == $inf) {
return "-";
}
else {
return sprintf "%10.2f%%", $p;
}
}
# NEW LEAKS
@keys = sort { $newMap{$b}{leakPercent} <=> $newMap{$a}{leakPercent} } keys %newMap;
my $needsHeading = 1;
my $total = 0;
foreach $key (@keys) {
my $percentLeaks = $newMap{$key}{leakPercent};
my $leaks = $newMap{$key}{leaked};
if ($percentLeaks > 0 && $key !~ /TOTAL/) {
if ($needsHeading) {
printf "--NEW-LEAKS-----------------------------------leaks------leaks%%-----------------------\n";
$needsHeading = 0;
}
printf "%-40s %10s %10s\n", ($key, $leaks, percentStr($percentLeaks));
$total += $leaks;
}
}
if (!$needsHeading) {
printf "%-40s %10s\n", ("TOTAL", $total);
}
# FIXED LEAKS
@keys = sort { $newMap{$b}{leakPercent} <=> $newMap{$a}{leakPercent} } keys %newMap;
$needsHeading = 1;
$total = 0;
foreach $key (@keys) {
my $percentLeaks = $newMap{$key}{leakPercent};
my $leaks = $newMap{$key}{leaked};
if ($percentLeaks < 0 && $key !~ /TOTAL/) {
if ($needsHeading) {
printf "--FIXED-LEAKS---------------------------------leaks------leaks%%-----------------------\n";
$needsHeading = 0;
}
printf "%-40s %10s %10s\n", ($key, $leaks, percentStr($percentLeaks));
$total += $leaks;
}
}
if (!$needsHeading) {
printf "%-40s %10s\n", ("TOTAL", $total);
}
# NEW BLOAT
@keys = sort { $newMap{$b}{bloatPercent} <=> $newMap{$a}{bloatPercent} } keys %newMap;
$needsHeading = 1;
$total = 0;
foreach $key (@keys) {
my $percentBloat = $newMap{$key}{bloatPercent};
my $bloat = $newMap{$key}{bloat};
if ($percentBloat > 0 && $key !~ /TOTAL/) {
if ($needsHeading) {
printf "--NEW-BLOAT-----------------------------------bloat------bloat%%-----------------------\n";
$needsHeading = 0;
}
printf "%-40s %10s %10s\n", ($key, $bloat, percentStr($percentBloat));
$total += $bloat;
}
}
if (!$needsHeading) {
printf "%-40s %10s\n", ("TOTAL", $total);
}
# ALL LEAKS
@keys = sort { $newMap{$b}{leaked} <=> $newMap{$a}{leaked} } keys %newMap;
$needsHeading = 1;
$total = 0;
foreach $key (@keys) {
my $leaks = $newMap{$key}{leaked};
my $percentLeaks = $newMap{$key}{leakPercent};
if ($leaks > 0) {
if ($needsHeading) {
printf "--ALL-LEAKS-----------------------------------leaks------leaks%%-----------------------\n";
$needsHeading = 0;
}
printf "%-40s %10s %10s\n", ($key, $leaks, percentStr($percentLeaks));
if ($key !~ /TOTAL/) {
$total += $leaks;
}
}
}
if (!$needsHeading) {
# printf "%-40s %10s\n", ("TOTAL", $total);
}
# ALL BLOAT
@keys = sort { $newMap{$b}{bloat} <=> $newMap{$a}{bloat} } keys %newMap;
$needsHeading = 1;
$total = 0;
foreach $key (@keys) {
my $bloat = $newMap{$key}{bloat};
my $percentBloat = $newMap{$key}{bloatPercent};
if ($bloat > 0) {
if ($needsHeading) {
printf "--ALL-BLOAT-----------------------------------bloat------bloat%%-----------------------\n";
$needsHeading = 0;
}
printf "%-40s %10s %10s\n", ($key, $bloat, percentStr($percentBloat));
if ($key !~ /TOTAL/) {
$total += $bloat;
}
}
}
if (!$needsHeading) {
# printf "%-40s %10s\n", ("TOTAL", $total);
}
# NEW CLASSES
@keys = sort { $newMap{$b}{bloatDelta} <=> $newMap{$a}{bloatDelta} } keys %newMap;
$needsHeading = 1;
my $ltotal = 0;
my $btotal = 0;
foreach $key (@keys) {
my $leaks = $newMap{$key}{leaked};
my $bloat = $newMap{$key}{bloat};
my $percentBloat = $newMap{$key}{bloatPercent};
if ($percentBloat == $inf && $key !~ /TOTAL/) {
if ($needsHeading) {
printf "--CLASSES-NOT-REPORTED-LAST-TIME--------------leaks------bloat------------------------\n";
$needsHeading = 0;
}
printf "%-40s %10s %10s\n", ($key, $leaks, $bloat);
if ($key !~ /TOTAL/) {
$ltotal += $leaks;
$btotal += $bloat;
}
}
}
if (!$needsHeading) {
printf "%-40s %10s %10s\n", ("TOTAL", $ltotal, $btotal);
}
# OLD CLASSES
@keys = sort { ($oldMap{$b}{bloat} || 0) <=> ($oldMap{$a}{bloat} || 0) } keys %oldMap;
$needsHeading = 1;
$ltotal = 0;
$btotal = 0;
foreach $key (@keys) {
if (!defined($newMap{$key})) {
my $leaks = $oldMap{$key}{leaked};
my $bloat = $oldMap{$key}{bloat};
if ($needsHeading) {
printf "--CLASSES-THAT-WENT-AWAY----------------------leaks------bloat------------------------\n";
$needsHeading = 0;
}
printf "%-40s %10s %10s\n", ($key, $leaks, $bloat);
if ($key !~ /TOTAL/) {
$ltotal += $leaks;
$btotal += $bloat;
}
}
}
if (!$needsHeading) {
printf "%-40s %10s %10s\n", ("TOTAL", $ltotal, $btotal);
}
print "--------------------------------------------------------------------------------------\n";

View file

@ -0,0 +1,590 @@
#!/usr/bin/perl -w
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# bloattable [-debug] [-source] [-byte n|-obj n|-ref n] <file1> <file2> ... <filen> > <html-file>
#
# file1, file2, ... filen should be successive BloatView files generated from the same run.
# Summarize them in an HTML table. Output the HTML to the standard output.
#
# If -debug is set, create a slightly larger html file which is more suitable for debugging this script.
# If -source is set, create an html file that prints the html source as the output
# If -byte n, -obj n, or -ref n is given, make the page default to showing byte, object, or reference statistics,
# respectively, and sort by the nth column (n is zero-based, so the first column has n==0).
#
# See http://lxr.mozilla.org/mozilla/source/xpcom/doc/MemoryTools.html
use 5.004;
use strict;
use diagnostics;
use File::Basename;
use Getopt::Long;
# The generated HTML is almost entirely generated by a script. Only the <HTML>, <HEAD>, and <BODY> elements are explicit
# because a <SCRIPT> element cannot officially be a direct descendant of an <HTML> element.
# The script itself is almost all generated by an eval of a large string. This allows the script to reproduce itself
# when making a new page using document.write's. Re-sorting the page causes it to regenerate itself in this way.
# Return the file's modification date.
sub fileModDate($) {
my ($pathName) = @_;
my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) =
stat $pathName or die "Can't stat '$pathName'";
return $mtime;
}
sub fileCoreName($) {
my ($pathName) = @_;
my $fileName = basename($pathName, "");
$fileName =~ s/\..*//;
return $fileName;
}
# Convert a raw string into a single-quoted JavaScript string.
sub singleQuoteString($) {
local ($_) = @_;
s/\\/\\\\/g;
s/'/\\'/g;
s/\n/\\n/g;
s/<\//<\\\//g;
return "'$_'";
}
# Convert a raw string into a double-quoted JavaScript string.
sub doubleQuoteString($) {
local ($_) = @_;
s/\\/\\\\/g;
s/"/\\"/g;
s/\n/\\n/g;
s/<\//<\\\//g;
return "\"$_\"";
}
# Quote special HTML characters in the string.
sub quoteHTML($) {
local ($_) = @_;
s/\&/&amp;/g;
s/</&lt;/g;
s/>/&gt;/g;
s/ /&nbsp;/g;
s/\n/<BR>\n/g;
return $_;
}
# Write the generated page to the standard output.
# The script source code is read from this file past the __END__ marker
# @$scriptData is the JavaScript source for the tables passed to JavaScript. Each entry is one line of JavaScript.
# @$persistentScriptData is the same as @scriptData, but persists when the page reloads itself.
# If $debug is true, generate the script directly instead of having it eval itself.
# If $source is true, generate a script that displays the page's source instead of the page itself.
sub generate(\@\@$$$$) {
my ($scriptData, $persistentScriptData, $debug, $source, $showMode, $sortColumn) = @_;
my @scriptSource = <DATA>;
chomp @scriptSource;
print <<'EOS';
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<HTML>
<HEAD>
<SCRIPT type="text/javascript">
EOS
foreach (@$scriptData) {print "$_\n";}
print "\n";
print "var srcArray = [\n";
my @quotedScriptSource = map {
my $line = $_;
$line =~ s/^\s+//g;
# $line =~ s/^\/\/SOURCE\s+//g if $source;
$line =~ s/^\/\/.*//g;
$line =~ s/\s+$//g;
$line eq "" ? () : $line
} @$persistentScriptData, @scriptSource;
my $lastQuotedLine = pop @quotedScriptSource;
foreach (@quotedScriptSource) {print doubleQuoteString($_), ",\n";}
print doubleQuoteString($lastQuotedLine), "];\n\n";
if ($debug) {
push @quotedScriptSource, $lastQuotedLine;
foreach (@quotedScriptSource) {
s/<\//<\\\//g; # This fails if a regexp ends with a '<'. Oh well....
print "$_\n";
}
print "\n";
} else {
print "eval(srcArray.join(\"\\n\"));\n\n";
}
print "showMode = $showMode;\n";
print "sortColumn = $sortColumn;\n";
if ($source) {
print <<'EOS';
function writeQuotedHTML(s) {
document.write(quoteHTML(s.toString()).replace(/\n/g, '<BR>\n'));
}
var quotingDocument = {
write: function () {
for (var i = 0; i < arguments.length; i++)
writeQuotedHTML(arguments[i]);
},
writeln: function () {
for (var i = 0; i < arguments.length; i++)
writeQuotedHTML(arguments[i]);
document.writeln('<BR>');
}
};
EOS
} else {
print "showHead(document);\n";
}
print "</SCRIPT>\n";
print "</HEAD>\n\n";
print "<BODY>\n";
if ($source) {
print "<P><TT>";
print quoteHTML "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n";
print quoteHTML "<HTML>\n";
print quoteHTML "<HEAD>\n";
print "<SCRIPT type=\"text/javascript\">showHead(quotingDocument);</SCRIPT>\n";
print quoteHTML "</HEAD>\n\n";
print quoteHTML "<BODY>\n";
print "<SCRIPT type=\"text/javascript\">showBody(quotingDocument);</SCRIPT>\n";
print quoteHTML "</BODY>\n";
print quoteHTML "</HTML>\n";
print "</TT></P>\n";
} else {
print "<SCRIPT type=\"text/javascript\">showBody(document);</SCRIPT>\n";
}
print "</BODY>\n";
print "</HTML>\n";
}
# Read the bloat file into hash table $h. The hash table is indexed by class names;
# each entry is a list with the following elements:
# bytesAlloc Total number of bytes allocated
# bytesNet Total number of bytes allocated but not deallocated
# objectsAlloc Total number of objects allocated
# objectsNet Total number of objects allocated but not deallocated
# refsAlloc Total number of references AddRef'd
# refsNet Total number of references AddRef'd but not Released
# Except for TOTAL, all hash table entries refer to mutually exclusive data.
# $sizes is a hash table indexed by class names. Each entry of that table contains the class's instance size.
sub readBloatFile($\%\%) {
my ($file, $h, $sizes) = @_;
local $_; # Needed for 'while (<FILE>)' below.
my $readSomething = 0;
open FILE, $file;
while (<FILE>) {
if (my ($name, $size, $bytesNet, $objectsAlloc, $objectsNet, $refsAlloc, $refsNet) =
/^\s*(?:\d+)\s+([\w:]+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(-?\d+)\s*\([^()]*\)\s*(\d+)\s+(-?\d+)\s*\([^()]*\)\s*$/) {
my $bytesAlloc;
if ($name eq "TOTAL") {
$size = "undefined";
$bytesAlloc = "undefined";
} else {
$bytesAlloc = $objectsAlloc * $size;
if ($bytesNet != $objectsNet * $size) {
print STDERR "In '$file', class $name bytesNet != objectsNet * size: $bytesNet != $objectsNet * $size\n";
}
}
print STDERR "Duplicate entry $name in '$file'\n" if $$h{$name};
$$h{$name} = [$bytesAlloc, $bytesNet, $objectsAlloc, $objectsNet, $refsAlloc, $refsNet];
my $oldSize = $$sizes{$name};
print STDERR "Mismatch of sizes of class $name: $oldSize and $size\n" if defined($oldSize) && $size ne $oldSize;
$$sizes{$name} = $size;
$readSomething = 1;
} elsif (/^\s*(?:\d+)\s+([\w:]+)\s/) {
print STDERR "Unable to parse '$file' line: $_";
}
}
close FILE;
print STDERR "No data in '$file'\n" unless $readSomething;
return $h;
}
my %sizes; # <class-name> => <instance-size>
my %tables; # <file-name> => <bloat-table>; see readBloatFile for format of <bloat-table>
# Generate the JavaScript source code for the row named $c. $l can contain the initial entries of the row.
sub genTableRowSource($$) {
my ($l, $c) = @_;
my $lastE;
foreach (@ARGV) {
my $e = $tables{$_}{$c};
if (defined($lastE) && !defined($e)) {
$e = [0,0,0,0,0,0];
print STDERR "Class $c is defined in an earlier file but not in '$_'\n";
}
if (defined $e) {
if (defined $lastE) {
for (my $i = 0; $i <= $#$e; $i++) {
my $n = $$e[$i];
$l .= ($n eq "undefined" ? "undefined" : $n - $$lastE[$i]) . ",";
}
$l .= " ";
} else {
$l .= join(",", @$e) . ", ";
}
$lastE = $e;
} else {
$l .= "0,0,0,0,0,0, ";
}
}
$l .= join(",", @$lastE);
return "[$l]";
}
my $debug;
my $source;
my $showMode;
my $sortColumn;
my @modeOptions;
GetOptions("debug" => \$debug, "source" => \$source, "byte=i" => \$modeOptions[0], "obj=i" => \$modeOptions[1], "ref=i" => \$modeOptions[2]);
for (my $i = 0; $i != 3; $i++) {
my $modeOption = $modeOptions[$i];
if ($modeOption) {
die "Only one of -byte, -obj, or -ref may be given" if defined $showMode;
my $nFileColumns = scalar(@ARGV) + 1;
die "-byte, -obj, or -ref column number out of range" if $modeOption < 0 || $modeOption >= 2 + 2*$nFileColumns;
$showMode = $i;
if ($modeOption >= 2) {
$modeOption -= 2;
$sortColumn = 2 + $showMode*2;
if ($modeOption >= $nFileColumns) {
$sortColumn++;
$modeOption -= $nFileColumns;
}
$sortColumn += $modeOption*6;
} else {
$sortColumn = $modeOption;
}
}
}
unless (defined $showMode) {
$showMode = 0;
$sortColumn = 0;
}
# Read all of the bloat files.
foreach (@ARGV) {
unless ($tables{$_}) {
my $f = $_;
my %table;
readBloatFile $_, %table, %sizes;
$tables{$_} = \%table;
}
}
die "No input" unless %sizes;
my @scriptData; # JavaScript source for the tables passed to JavaScript. Each entry is one line of JavaScript.
my @persistentScriptData; # Same as @scriptData, but persists the page reloads itself.
# Print a list of bloat file names.
push @persistentScriptData, "var nFiles = " . scalar(@ARGV) . ";";
push @persistentScriptData, "var fileTags = [" . join(", ", map {singleQuoteString substr(fileCoreName($_), -10)} @ARGV) . "];";
push @persistentScriptData, "var fileNames = [" . join(", ", map {singleQuoteString $_} @ARGV) . "];";
push @persistentScriptData, "var fileDates = [" . join(", ", map {singleQuoteString localtime fileModDate $_} @ARGV) . "];";
# Print the bloat tables.
push @persistentScriptData, "var totals = " . genTableRowSource('"TOTAL", undefined, ', "TOTAL") . ";";
push @scriptData, "var classTables = [";
delete $sizes{"TOTAL"};
my @classes = sort(keys %sizes);
for (my $i = 0; $i <= $#classes; $i++) {
my $c = $classes[$i];
push @scriptData, genTableRowSource(doubleQuoteString($c).", ".$sizes{$c}.", ", $c) . ($i == $#classes ? "];" : ",");
}
generate(@scriptData, @persistentScriptData, $debug, $source, $showMode, $sortColumn);
1;
# The source of the eval'd JavaScript follows.
# Comments starting with // that are alone on a line are stripped by the Perl script.
__END__
// showMode: 0=bytes, 1=objects, 2=references
var showMode;
var modeName;
var modeNameUpper;
var sortColumn;
// Sort according to the sortColumn. Column 0 is sorted alphabetically in ascending order.
// All other columns are sorted numerically in descending order, with column 0 used for a secondary sort.
// Undefined is always listed last.
function sortCompare(x, y) {
if (sortColumn) {
var xc = x[sortColumn];
var yc = y[sortColumn];
if (xc < yc || xc === undefined && yc !== undefined) return 1;
if (yc < xc || yc === undefined && xc !== undefined) return -1;
}
var x0 = x[0];
var y0 = y[0];
if (x0 > y0 || x0 === undefined && y0 !== undefined) return 1;
if (y0 > x0 || y0 === undefined && x0 !== undefined) return -1;
return 0;
}
// Quote special HTML characters in the string.
function quoteHTML(s) {
s = s.replace(/&/g, '&amp;');
// Can't use /</g because HTML interprets '</g' as ending the script!
s = s.replace(/\x3C/g, '&lt;');
s = s.replace(/>/g, '&gt;');
s = s.replace(/ /g, '&nbsp;');
return s;
}
function writeFileTable(d) {
d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>');
d.writeln('<TR>\n<TH>Name</TH>\n<TH>File</TH>\n<TH>Date</TH>\n</TR>');
for (var i = 0; i < nFiles; i++)
d.writeln('<TR>\n<TD>'+quoteHTML(fileTags[i])+'</TD>\n<TD><TT>'+quoteHTML(fileNames[i])+'</TT></TD>\n<TD>'+quoteHTML(fileDates[i])+'</TD>\n</TR>');
d.writeln('</TABLE>');
}
function writeReloadLink(d, column, s, rowspan) {
d.write(rowspan == 1 ? '<TH>' : '<TH rowspan='+rowspan+'>');
if (column != sortColumn)
d.write('<A href="javascript:reloadSelf('+column+','+showMode+')">');
d.write(s);
if (column != sortColumn)
d.write('</A>');
d.writeln('</TH>');
}
function writeClassTableRow(d, row, base, modeName) {
if (modeName) {
d.writeln('<TR>\n<TH>'+modeName+'</TH>');
} else {
d.writeln('<TR>\n<TD><A href="javascript:showRowDetail(\''+row[0]+'\')">'+quoteHTML(row[0])+'</A></TD>');
var v = row[1];
d.writeln('<TD class=num>'+(v === undefined ? '' : v)+'</TD>');
}
for (var i = 0; i != 2; i++) {
var c = base + i;
for (var j = 0; j <= nFiles; j++) {
v = row[c];
var style = 'num';
if (j != nFiles)
if (v > 0) {
style = 'pos';
v = '+'+v;
} else
style = 'neg';
d.writeln('<TD class='+style+'>'+(v === undefined ? '' : v)+'</TD>');
c += 6;
}
}
d.writeln('</TR>');
}
function writeClassTable(d) {
var base = 2 + showMode*2;
// Make a copy because a sort is destructive.
var table = classTables.concat();
table.sort(sortCompare);
d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>');
d.writeln('<TR>');
writeReloadLink(d, 0, 'Class Name', 2);
writeReloadLink(d, 1, 'Instance<BR>Size', 2);
d.writeln('<TH colspan='+(nFiles+1)+'>'+modeNameUpper+'s allocated</TH>');
d.writeln('<TH colspan='+(nFiles+1)+'>'+modeNameUpper+'s allocated but not freed</TH>\n</TR>');
d.writeln('<TR>');
for (var i = 0; i != 2; i++) {
var c = base + i;
for (var j = 0; j <= nFiles; j++) {
writeReloadLink(d, c, j == nFiles ? 'Total' : quoteHTML(fileTags[j]), 1);
c += 6;
}
}
d.writeln('</TR>');
writeClassTableRow(d, totals, base, 0);
for (var r = 0; r < table.length; r++)
writeClassTableRow(d, table[r], base, 0);
d.writeln('</TABLE>');
}
var modeNames = ["byte", "object", "reference"];
var modeNamesUpper = ["Byte", "Object", "Reference"];
var styleSheet = '<STYLE type="TEXT/CSS">\n'+
'BODY {background-color: #FFFFFF; color: #000000}\n'+
'.num {text-align: right}\n'+
'.pos {text-align: right; color: #CC0000}\n'+
'.neg {text-align: right; color: #009900}\n'+
'</STYLE>';
function showHead(d) {
modeName = modeNames[showMode];
modeNameUpper = modeNamesUpper[showMode];
d.writeln('<TITLE>'+modeNameUpper+' Bloats</TITLE>');
d.writeln(styleSheet);
}
function showBody(d) {
d.writeln('<H1>'+modeNameUpper+' Bloats</H1>');
writeFileTable(d);
d.write('<FORM>');
for (var i = 0; i != 3; i++)
if (i != showMode) {
var newSortColumn = sortColumn;
if (sortColumn >= 2)
newSortColumn = sortColumn + (i-showMode)*2;
d.write('<INPUT type="button" value="Show '+modeNamesUpper[i]+'s" onClick="reloadSelf('+newSortColumn+','+i+')">');
}
d.writeln('</FORM>');
d.writeln('<P>The numbers do not include <CODE>malloc</CODE>\'d data such as string contents.</P>');
d.writeln('<P>Click on a column heading to sort by that column. Click on a class name to see details for that class.</P>');
writeClassTable(d);
}
function showRowDetail(rowName) {
var row;
var i;
if (rowName == "TOTAL")
row = totals;
else {
for (i = 0; i < classTables.length; i++)
if (rowName == classTables[i][0]) {
row = classTables[i];
break;
}
}
if (row) {
var w = window.open("", "ClassTableRowDetails");
var d = w.document;
d.open();
d.writeln('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">');
d.writeln('<HTML>\n<HEAD>\n<TITLE>'+quoteHTML(rowName)+' bloat details</TITLE>');
d.writeln(styleSheet);
d.writeln('</HEAD>\n\n<BODY>');
d.writeln('<H2>'+quoteHTML(rowName)+'</H2>');
if (row[1] !== undefined)
d.writeln('<P>Each instance has '+row[1]+' bytes.</P>');
d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>');
d.writeln('<TR>\n<TH></TH>\n<TH colspan='+(nFiles+1)+'>Allocated</TH>');
d.writeln('<TH colspan='+(nFiles+1)+'>Allocated but not freed</TH>\n</TR>');
d.writeln('<TR>\n<TH></TH>');
for (i = 0; i != 2; i++)
for (var j = 0; j <= nFiles; j++)
d.writeln('<TH>'+(j == nFiles ? 'Total' : quoteHTML(fileTags[j]))+'</TH>');
d.writeln('</TR>');
for (i = 0; i != 3; i++)
writeClassTableRow(d, row, 2+i*2, modeNamesUpper[i]+'s');
d.writeln('</TABLE>\n</BODY>\n</HTML>');
d.close();
}
return undefined;
}
function stringSource(s) {
s = s.replace(/\\/g, '\\\\');
s = s.replace(/"/g, '\\"');
s = s.replace(/<\//g, '<\\/');
return '"'+s+'"';
}
function reloadSelf(n,m) {
// Need to cache these because globals go away on document.open().
var sa = srcArray;
var ss = stringSource;
var ct = classTables;
var i;
document.open();
// Uncomment this and comment the document.open() line above to see the reloaded page's source.
//var w = window.open("", "NewDoc");
//var d = w.document;
//var document = new Object;
//document.write = function () {
// for (var i = 0; i < arguments.length; i++) {
// var s = arguments[i].toString();
// s = s.replace(/&/g, '&amp;');
// s = s.replace(/\x3C/g, '&lt;');
// s = s.replace(/>/g, '&gt;');
// s = s.replace(/ /g, '&nbsp;');
// d.write(s);
// }
//};
//document.writeln = function () {
// for (var i = 0; i < arguments.length; i++) {
// var s = arguments[i].toString();
// s = s.replace(/&/g, '&amp;');
// s = s.replace(/\x3C/g, '&lt;');
// s = s.replace(/>/g, '&gt;');
// s = s.replace(/ /g, '&nbsp;');
// d.write(s);
// }
// d.writeln('<BR>');
//};
document.writeln('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">');
document.writeln('<HTML>\n<HEAD>\n<SCRIPT type="text/javascript">');
// Manually copy non-persistent script data
if (!ct.length)
document.writeln('var classTables = [];');
else {
document.writeln('var classTables = [');
for (i = 0; i < ct.length; i++) {
var row = ct[i];
document.write('[' + ss(row[0]));
for (var j = 1; j < row.length; j++)
document.write(',' + row[j]);
document.writeln(']' + (i == ct.length-1 ? '];' : ','));
}
}
document.writeln('var srcArray = [');
for (i = 0; i < sa.length; i++) {
document.write(ss(sa[i]));
if (i != sa.length-1)
document.writeln(',');
}
document.writeln('];');
document.writeln('eval(srcArray.join("\\n"));');
document.writeln('showMode = '+m+';');
document.writeln('sortColumn = '+n+';');
document.writeln('showHead(document);');
document.writeln('</SCRIPT>\n</HEAD>\n\n<BODY>\n<SCRIPT type="text/javascript">showBody(document);</SCRIPT>\n</BODY>\n</HTML>');
document.close();
return undefined;
}

View file

@ -0,0 +1,172 @@
#!/usr/bin/python
#
# Copyright (C) 2009 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Script to run the linter for source code of WebKit."""
import os
import os.path
import re
import sys
import modules.cpplint as cpplint
from modules.diff_parser import DiffParser
from modules.scm import detect_scm_system
# Override the usage of the lint tool.
cpplint._USAGE = """
Syntax: %(program_name)s [--verbose=#] [--git-commit=<COMMITISH>] [--output=vs7] [--filter=-x,+y,...]
The style guidelines this tries to follow are those in
http://webkit.org/coding/coding-style.html
Every problem is given a confidence score from 1-5, with 5 meaning we are
certain of the problem, and 1 meaning it could be a legitimate construct.
This will miss some errors, and is not a substitute for a code review.
To prevent specific lines from being linted, add a '// NOLINT' comment to the
end of the line.
Linted extensions are .cpp, .c and .h. Other file types will be ignored.
Flags:
verbose=#
Specify a number 0-5 to restrict errors to certain verbosity levels.
git-commit=<COMMITISH>
Check style for a specified git commit.
Note that the program checks style based on current local file
instead of actual diff of the git commit. So, if the files are
updated after the specified git commit, the information of line
number may be wrong.
output=vs7
By default, the output is formatted to ease emacs parsing. Visual Studio
compatible output (vs7) may also be used. Other formats are unsupported.
filter=-x,+y,...
Specify a comma-separated list of category-filters to apply: only
error messages whose category names pass the filters will be printed.
(Category names are printed with the message and look like
"[whitespace/indent]".) Filters are evaluated left to right.
"-FOO" and "FOO" means "do not print categories that start with FOO".
"+FOO" means "do print categories that start with FOO".
Examples: --filter=-whitespace,+whitespace/braces
--filter=whitespace,runtime/printf,+runtime/printf_format
--filter=-,+build/include_what_you_use
To see a list of all the categories used in %(program_name)s, pass no arg:
--filter=
""" % {'program_name': sys.argv[0]}
def process_patch(patch_string, root, cwd, scm):
"""Does lint on a single patch.
Args:
patch_string: A string of a patch.
"""
patch = DiffParser(patch_string.splitlines())
if not len(patch.files):
cpplint.error("patch", 0, "patch/notempty", 3,
"Patch does not appear to diff against any file.")
return
if not patch.status_line:
cpplint.error("patch", 0, "patch/nosummary", 3,
"Patch does not have a summary.")
else:
proper_format = re.match(r"^Bug [0-9]+ - ", patch.status_line)
if not proper_format:
proper_format = re.match(r"^No bug - ", patch.status_line)
cpplint.error("patch", 0, "patch/bugnumber", 3,
"Patch summary should begin with 'Bug XXXXX - ' " +
"or 'No bug -'.")
if not patch.patch_description:
cpplint.error("patch", 0, "patch/nodescription", 3,
"Patch does not have a description.")
for filename, diff in patch.files.iteritems():
file_extension = os.path.splitext(filename)[1]
if file_extension in ['.cpp', '.c', '.h']:
line_numbers = set()
orig_filename = filename
def error_for_patch(filename, line_number, category, confidence,
message):
"""Wrapper function of cpplint.error for patches.
This function outputs errors only if the line number
corresponds to lines which are modified or added.
"""
if not line_numbers:
for line in diff.lines:
# When deleted line is not set, it means that
# the line is newly added.
if not line[0]:
line_numbers.add(line[1])
if line_number in line_numbers:
cpplint.error(orig_filename, line_number,
category, confidence, message)
cpplint.process_file(os.path.join(root, filename),
relative_name=orig_filename,
error=error_for_patch)
def main():
cpplint.use_mozilla_styles()
(args, flags) = cpplint.parse_arguments(sys.argv[1:], ["git-commit="])
if args:
sys.stderr.write("ERROR: We don't support files as arguments for " +
"now.\n" + cpplint._USAGE)
sys.exit(1)
cwd = os.path.abspath('.')
scm = detect_scm_system(cwd)
root = scm.find_checkout_root(cwd)
if "--git-commit" in flags:
process_patch(scm.create_patch_from_local_commit(flags["--git-commit"]), root, cwd, scm)
else:
process_patch(scm.create_patch(), root, cwd, scm)
sys.stderr.write('Total errors found: %d\n' % cpplint.error_count())
sys.exit(cpplint.error_count() > 0)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,162 @@
# Copyright (C) 2009 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""WebKit's Python module for interacting with patches."""
import logging
import re
_regexp_compile_cache = {}
def match(pattern, string):
"""Matches the string with the pattern, caching the compiled regexp."""
if not pattern in _regexp_compile_cache:
_regexp_compile_cache[pattern] = re.compile(pattern)
return _regexp_compile_cache[pattern].match(string)
def git_diff_to_svn_diff(line):
"""Converts a git formatted diff line to a svn formatted line.
Args:
line: A string representing a line of the diff.
"""
conversion_patterns = (("^diff --git a/(.+) b/(?P<FilePath>.+)", lambda matched: "Index: " + matched.group('FilePath') + "\n"),
("^new file.*", lambda matched: "\n"),
("^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}", lambda matched: "===================================================================\n"),
("^--- a/(?P<FilePath>.+)", lambda matched: "--- " + matched.group('FilePath') + "\n"),
("^\+\+\+ b/(?P<FilePath>.+)", lambda matched: "+++ " + matched.group('FilePath') + "\n"))
for pattern, conversion in conversion_patterns:
matched = match(pattern, line)
if matched:
return conversion(matched)
return line
def get_diff_converter(first_diff_line):
"""Gets a converter function of diff lines.
Args:
first_diff_line: The first filename line of a diff file.
If this line is git formatted, we'll return a
converter from git to SVN.
"""
if match(r"^diff --git a/", first_diff_line):
return git_diff_to_svn_diff
return lambda input: input
_INITIAL_STATE = 1
_DECLARED_FILE_PATH = 2
_PROCESSING_CHUNK = 3
class DiffFile:
"""Contains the information for one file in a patch.
The field "lines" is a list which contains tuples in this format:
(deleted_line_number, new_line_number, line_string)
If deleted_line_number is zero, it means this line is newly added.
If new_line_number is zero, it means this line is deleted.
"""
def __init__(self, filename):
self.filename = filename
self.lines = []
def add_new_line(self, line_number, line):
self.lines.append((0, line_number, line))
def add_deleted_line(self, line_number, line):
self.lines.append((line_number, 0, line))
def add_unchanged_line(self, deleted_line_number, new_line_number, line):
self.lines.append((deleted_line_number, new_line_number, line))
class DiffParser:
"""A parser for a patch file.
The field "files" is a dict whose key is the filename and value is
a DiffFile object.
"""
def __init__(self, diff_input):
"""Parses a diff.
Args:
diff_input: An iterable object.
"""
state = _INITIAL_STATE
self.files = {}
current_file = None
old_diff_line = None
new_diff_line = None
for line in diff_input:
line = line.rstrip("\n")
if state == _INITIAL_STATE:
transform_line = get_diff_converter(line)
line = transform_line(line)
file_declaration = match(r"^Index: (?P<FilePath>.+)", line)
if file_declaration:
filename = file_declaration.group('FilePath')
current_file = DiffFile(filename)
self.files[filename] = current_file
state = _DECLARED_FILE_PATH
continue
lines_changed = match(r"^@@ -(?P<OldStartLine>\d+)(,\d+)? \+(?P<NewStartLine>\d+)(,\d+)? @@", line)
if lines_changed:
if state != _DECLARED_FILE_PATH and state != _PROCESSING_CHUNK:
logging.error('Unexpected line change without file path declaration: %r' % line)
old_diff_line = int(lines_changed.group('OldStartLine'))
new_diff_line = int(lines_changed.group('NewStartLine'))
state = _PROCESSING_CHUNK
continue
if state == _PROCESSING_CHUNK:
if line.startswith('+'):
current_file.add_new_line(new_diff_line, line[1:])
new_diff_line += 1
elif line.startswith('-'):
current_file.add_deleted_line(old_diff_line, line[1:])
old_diff_line += 1
elif line.startswith(' '):
current_file.add_unchanged_line(old_diff_line, new_diff_line, line[1:])
old_diff_line += 1
new_diff_line += 1
elif line == '\\ No newline at end of file':
# Nothing to do. We may still have some added lines.
pass
else:
logging.error('Unexpected diff format when parsing a chunk: %r' % line)

View file

@ -0,0 +1 @@
# Required for Python to search this directory for module files

3150
tools/check-moz-style/modules/cpplint.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,180 @@
# Copyright (C) 2009 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""WebKit's Python module for interacting with patches."""
import logging
import re
_regexp_compile_cache = {}
def match(pattern, string):
"""Matches the string with the pattern, caching the compiled regexp."""
if not pattern in _regexp_compile_cache:
_regexp_compile_cache[pattern] = re.compile(pattern)
return _regexp_compile_cache[pattern].match(string)
def git_diff_to_svn_diff(line):
"""Converts a git formatted diff line to a svn formatted line.
Args:
line: A string representing a line of the diff.
"""
conversion_patterns = (("^diff --git a/(.+) b/(?P<FilePath>.+)", lambda matched: "Index: " + matched.group('FilePath') + "\n"),
("^new file.*", lambda matched: "\n"),
("^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}", lambda matched: "===================================================================\n"),
("^--- a/(?P<FilePath>.+)", lambda matched: "--- " + matched.group('FilePath') + "\n"),
("^\+\+\+ b/(?P<FilePath>.+)", lambda matched: "+++ " + matched.group('FilePath') + "\n"))
for pattern, conversion in conversion_patterns:
matched = match(pattern, line)
if matched:
return conversion(matched)
return line
def get_diff_converter(first_diff_line):
"""Gets a converter function of diff lines.
Args:
first_diff_line: The first filename line of a diff file.
If this line is git formatted, we'll return a
converter from git to SVN.
"""
if match(r"^diff --git a/", first_diff_line):
return git_diff_to_svn_diff
return lambda input: input
_INITIAL_STATE = 1
_DECLARED_FILE_PATH = 2
_PROCESSING_CHUNK = 3
class DiffFile:
"""Contains the information for one file in a patch.
The field "lines" is a list which contains tuples in this format:
(deleted_line_number, new_line_number, line_string)
If deleted_line_number is zero, it means this line is newly added.
If new_line_number is zero, it means this line is deleted.
"""
def __init__(self, filename):
self.filename = filename
self.lines = []
def add_new_line(self, line_number, line):
self.lines.append((0, line_number, line))
def add_deleted_line(self, line_number, line):
self.lines.append((line_number, 0, line))
def add_unchanged_line(self, deleted_line_number, new_line_number, line):
self.lines.append((deleted_line_number, new_line_number, line))
class DiffParser:
"""A parser for a patch file.
The field "files" is a dict whose key is the filename and value is
a DiffFile object.
"""
def __init__(self, diff_input):
"""Parses a diff.
Args:
diff_input: An iterable object.
"""
state = _INITIAL_STATE
self.files = {}
self.status_line = None
self.patch_description = None
current_file = None
old_diff_line = None
new_diff_line = None
for line in diff_input:
line = line.rstrip("\n")
if state == _INITIAL_STATE:
transform_line = get_diff_converter(line)
line = transform_line(line)
comment_line = match(r"^\#", line)
if comment_line:
continue
file_declaration = match(r"^Index: (?P<FilePath>.+)", line)
if file_declaration:
filename = file_declaration.group('FilePath')
current_file = DiffFile(filename)
self.files[filename] = current_file
state = _DECLARED_FILE_PATH
continue
lines_changed = match(r"^@@ -(?P<OldStartLine>\d+)(,\d+)? \+(?P<NewStartLine>\d+)(,\d+)? @@", line)
if lines_changed:
if state != _DECLARED_FILE_PATH and state != _PROCESSING_CHUNK:
logging.error('Unexpected line change without file path declaration: %r' % line)
old_diff_line = int(lines_changed.group('OldStartLine'))
new_diff_line = int(lines_changed.group('NewStartLine'))
state = _PROCESSING_CHUNK
continue
if state == _PROCESSING_CHUNK:
if line.startswith('+'):
current_file.add_new_line(new_diff_line, line[1:])
new_diff_line += 1
elif line.startswith('-'):
current_file.add_deleted_line(old_diff_line, line[1:])
old_diff_line += 1
elif line.startswith(' '):
current_file.add_unchanged_line(old_diff_line, new_diff_line, line[1:])
old_diff_line += 1
new_diff_line += 1
elif line == '\\ No newline at end of file':
# Nothing to do. We may still have some added lines.
pass
else:
logging.error('Unexpected diff format when parsing a chunk: %r' % line)
# Patch description
if state == _INITIAL_STATE:
if not self.status_line:
self.status_line = line
else:
if not self.patch_description:
# Skip the first blank line after the patch description
if line != "":
self.patch_description = line
else:
self.patch_description = self.patch_description + "\n" + line

View file

@ -0,0 +1,39 @@
# Copyright (c) 2009, Google Inc. All rights reserved.
# Copyright (c) 2009 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# WebKit's Python module for logging
import sys
def log(string):
print >> sys.stderr, string
def error(string):
log("ERROR: " + string)
exit(1)

View file

@ -0,0 +1,420 @@
# Copyright (c) 2009, Google Inc. All rights reserved.
# Copyright (c) 2009 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Python module for interacting with an SCM system (like SVN or Git)
import os
import re
import subprocess
import sys
# Import WebKit-specific modules.
from modules.logging import error, log
def detect_scm_system(path):
if HG.in_working_directory(path):
return HG(cwd=path)
if SVN.in_working_directory(path):
return SVN(cwd=path)
if Git.in_working_directory(path):
return Git(cwd=path)
raise ScriptError("working directory is not a HG/SVN/Git repo")
def first_non_empty_line_after_index(lines, index=0):
first_non_empty_line = index
for line in lines[index:]:
if re.match("^\s*$", line):
first_non_empty_line += 1
else:
break
return first_non_empty_line
class CommitMessage:
def __init__(self, message):
self.message_lines = message[first_non_empty_line_after_index(message, 0):]
def body(self, lstrip=False):
lines = self.message_lines[first_non_empty_line_after_index(self.message_lines, 1):]
if lstrip:
lines = [line.lstrip() for line in lines]
return "\n".join(lines) + "\n"
def description(self, lstrip=False, strip_url=False):
line = self.message_lines[0]
if lstrip:
line = line.lstrip()
if strip_url:
line = re.sub("^(\s*)<.+> ", "\1", line)
return line
def message(self):
return "\n".join(self.message_lines) + "\n"
def parse_bug_id(self):
for line in self.message_lines:
match = re.search("http\://webkit\.org/b/(?P<bug_id>\d+)", line)
if match:
return match.group('bug_id')
match = re.search(Bugzilla.bug_server_regex + "show_bug\.cgi\?id=(?P<bug_id>\d+)", line)
if match:
return match.group('bug_id')
return None
class ScriptError(Exception):
pass
class SCM:
def __init__(self, cwd, dryrun=False):
self.cwd = cwd
self.checkout_root = self.find_checkout_root(self.cwd)
self.dryrun = dryrun
@staticmethod
def run_command(args, cwd=None, input=None, raise_on_failure=True, return_exit_code=False):
stdin = subprocess.PIPE if input else None
process = subprocess.Popen(args, stdout=subprocess.PIPE, stdin=stdin, cwd=cwd)
output = process.communicate(input)[0].rstrip()
exit_code = process.wait()
if raise_on_failure and exit_code:
raise ScriptError('Failed to run "%s" exit_code: %d cwd: %s' % (args, exit_code, cwd))
if return_exit_code:
return exit_code
return output
def script_path(self, script_name):
return os.path.join(self.checkout_root, "WebKitTools", "Scripts", script_name)
def ensure_clean_working_directory(self, force):
if not force and not self.working_directory_is_clean():
print self.run_command(self.status_command(), raise_on_failure=False)
error("Working directory has modifications, pass --force-clean or --no-clean to continue.")
log("Cleaning working directory")
self.clean_working_directory()
def ensure_no_local_commits(self, force):
if not self.supports_local_commits():
return
commits = self.local_commits()
if not len(commits):
return
if not force:
error("Working directory has local commits, pass --force-clean to continue.")
self.discard_local_commits()
def apply_patch(self, patch):
# It's possible that the patch was not made from the root directory.
# We should detect and handle that case.
curl_process = subprocess.Popen(['curl', patch['url']], stdout=subprocess.PIPE)
patch_apply_process = subprocess.Popen([self.script_path('svn-apply'), '--reviewer', patch['reviewer']], stdin=curl_process.stdout)
return_code = patch_apply_process.wait()
if return_code:
raise ScriptError("Patch %s from bug %s failed to download and apply." % (patch['url'], patch['bug_id']))
def run_status_and_extract_filenames(self, status_command, status_regexp):
filenames = []
for line in self.run_command(status_command).splitlines():
match = re.search(status_regexp, line)
if not match:
continue
# status = match.group('status')
filename = match.group('filename')
filenames.append(filename)
return filenames
@staticmethod
def in_working_directory(path):
raise NotImplementedError, "subclasses must implement"
@staticmethod
def find_checkout_root(path):
raise NotImplementedError, "subclasses must implement"
@staticmethod
def commit_success_regexp():
raise NotImplementedError, "subclasses must implement"
def working_directory_is_clean(self):
raise NotImplementedError, "subclasses must implement"
def clean_working_directory(self):
raise NotImplementedError, "subclasses must implement"
def update_webkit(self):
raise NotImplementedError, "subclasses must implement"
def status_command(self):
raise NotImplementedError, "subclasses must implement"
def changed_files(self):
raise NotImplementedError, "subclasses must implement"
def display_name(self):
raise NotImplementedError, "subclasses must implement"
def create_patch(self):
raise NotImplementedError, "subclasses must implement"
def commit_with_message(self, message):
raise NotImplementedError, "subclasses must implement"
# Subclasses must indicate if they support local commits,
# but the SCM baseclass will only call local_commits methods when this is true.
@staticmethod
def supports_local_commits():
raise NotImplementedError, "subclasses must implement"
def create_patch_from_local_commit(self, commit_id):
pass
def commit_locally_with_message(self, message):
pass
def discard_local_commits(self):
pass
def local_commits(self):
return []
class SVN(SCM):
def __init__(self, cwd, dryrun=False):
SCM.__init__(self, cwd, dryrun)
self.cached_version = None
@staticmethod
def in_working_directory(path):
return os.path.isdir(os.path.join(path, '.svn'))
@staticmethod
def find_uuid(path):
if not SVN.in_working_directory(path):
return None
info = SVN.run_command(['svn', 'info', path])
match = re.search("^Repository UUID: (?P<uuid>.+)$", info, re.MULTILINE)
if not match:
raise ScriptError('svn info did not contain a UUID.')
return match.group('uuid')
@staticmethod
def find_checkout_root(path):
uuid = SVN.find_uuid(path)
# If |path| is not in a working directory, we're supposed to return |path|.
if not uuid:
return path
# Search up the directory hierarchy until we find a different UUID.
last_path = None
while True:
if uuid != SVN.find_uuid(path):
return last_path
last_path = path
(path, last_component) = os.path.split(path)
if last_path == path:
return None
@staticmethod
def commit_success_regexp():
return "^Committed revision (?P<svn_revision>\d+)\.$"
def svn_version(self):
if not self.cached_version:
self.cached_version = self.run_command(['svn', '--version', '--quiet'])
return self.cached_version
def working_directory_is_clean(self):
return self.run_command(['svn', 'diff']) == ""
def clean_working_directory(self):
self.run_command(['svn', 'revert', '-R', '.'])
def update_webkit(self):
self.run_command(self.script_path("update-webkit"))
def status_command(self):
return ['svn', 'status']
def changed_files(self):
if self.svn_version() > "1.6":
status_regexp = "^(?P<status>[ACDMR]).{6} (?P<filename>.+)$"
else:
status_regexp = "^(?P<status>[ACDMR]).{5} (?P<filename>.+)$"
return self.run_status_and_extract_filenames(self.status_command(), status_regexp)
@staticmethod
def supports_local_commits():
return False
def display_name(self):
return "svn"
def create_patch(self):
return self.run_command(self.script_path("svn-create-patch"))
def commit_with_message(self, message):
if self.dryrun:
return "Dry run, no remote commit."
return self.run_command(['svn', 'commit', '-m', message])
# All git-specific logic should go here.
class Git(SCM):
def __init__(self, cwd, dryrun=False):
SCM.__init__(self, cwd, dryrun)
@classmethod
def in_working_directory(cls, path):
return cls.run_command(['git', 'rev-parse', '--is-inside-work-tree'], raise_on_failure=False, cwd=path) == "true"
@classmethod
def find_checkout_root(cls, path):
# "git rev-parse --show-cdup" would be another way to get to the root
(checkout_root, dot_git) = os.path.split(cls.run_command(['git', 'rev-parse', '--git-dir'], cwd=path))
# If we were using 2.6 # checkout_root = os.path.relpath(checkout_root, path)
if not os.path.isabs(checkout_root): # Sometimes git returns relative paths
checkout_root = os.path.join(path, checkout_root)
return checkout_root
@staticmethod
def commit_success_regexp():
return "^Committed r(?P<svn_revision>\d+)$"
def discard_local_commits(self):
self.run_command(['git', 'reset', '--hard', 'trunk'])
def local_commits(self):
return self.run_command(['git', 'log', '--pretty=oneline', 'HEAD...trunk']).splitlines()
def working_directory_is_clean(self):
return self.run_command(['git', 'diff-index', 'HEAD']) == ""
def clean_working_directory(self):
# Could run git clean here too, but that wouldn't match working_directory_is_clean
self.run_command(['git', 'reset', '--hard', 'HEAD'])
def update_webkit(self):
# FIXME: Should probably call update-webkit, no?
log("Updating working directory")
self.run_command(['git', 'svn', 'rebase'])
def status_command(self):
return ['git', 'status']
def changed_files(self):
status_command = ['git', 'diff', '-r', '--name-status', '-C', '-M', 'HEAD']
status_regexp = '^(?P<status>[ADM])\t(?P<filename>.+)$'
return self.run_status_and_extract_filenames(status_command, status_regexp)
@staticmethod
def supports_local_commits():
return True
def display_name(self):
return "git"
def create_patch(self):
return self.run_command(['git', 'diff', 'HEAD'])
def commit_with_message(self, message):
self.commit_locally_with_message(message)
return self.push_local_commits_to_server()
# Git-specific methods:
def create_patch_from_local_commit(self, commit_id):
return self.run_command(['git', 'diff', commit_id + "^.." + commit_id])
def commit_locally_with_message(self, message):
self.run_command(['git', 'commit', '--all', '-F', '-'], input=message)
def push_local_commits_to_server(self):
if self.dryrun:
return "Dry run, no remote commit."
return self.run_command(['git', 'svn', 'dcommit'])
def commit_ids_from_range_arguments(self, args, cherry_pick=False):
# First get the commit-ids for the passed in revisions.
revisions = self.run_command(['git', 'rev-parse', '--revs-only'] + args).splitlines()
if cherry_pick:
return revisions
# If we're not cherry picking and were only passed one revision, assume "^revision head" aka "revision..head".
if len(revisions) < 2:
revisions[0] = "^" + revisions[0]
revisions.append("HEAD")
return self.run_command(['git', 'rev-list'] + revisions).splitlines()
def commit_message_for_local_commit(self, commit_id):
commit_lines = self.run_command(['git', 'cat-file', 'commit', commit_id]).splitlines()
# Skip the git headers.
first_line_after_headers = 0
for line in commit_lines:
first_line_after_headers += 1
if line == "":
break
return CommitMessage(commit_lines[first_line_after_headers:])
def files_changed_summary_for_commit(self, commit_id):
return self.run_command(['git', 'diff-tree', '--shortstat', '--no-commit-id', commit_id])
# All hg-specific logic should go here.
class HG(SCM):
def __init__(self, cwd, dryrun=False):
SCM.__init__(self, cwd, dryrun)
@classmethod
def in_working_directory(cls, path):
return cls.run_command(['hg', 'status'], cwd=path, return_exit_code=True) == 0
@classmethod
def find_checkout_root(cls, path):
checkout_root = cls.run_command(['hg', 'root'], cwd=path)
return checkout_root
def status_command(self):
return ['hg', 'status']
def display_name(self):
return "hg"
def create_patch(self):
if self.run_command(['hg', 'diff']) != "":
sys.stderr.write("Warning: outstanding changes not include in style check.\n")
return self.run_command(['hg', 'export', 'tip'])

View file

@ -0,0 +1,78 @@
#!/usr/bin/python
#
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
#
from __future__ import print_function
from modules.scm import detect_scm_system
from contextlib import closing
import checkmozstyle
import os
import modules.cpplint as cpplint
import StringIO
TESTS = [
# Empty patch
{
"patch": "tests/test1.patch",
"cpp": "tests/test1.cpp",
"out": "tests/test1.out"
},
# Bad header
{
"patch": "tests/test2.patch",
"cpp": "tests/test2.cpp",
"out": "tests/test2.out"
},
# Bad Description
{
"patch": "tests/test3.patch",
"cpp": "tests/test3.cpp",
"out": "tests/test3.out"
},
# readability tests
{
"patch": "tests/test4.patch",
"cpp": "tests/test4.cpp",
"out": "tests/test4.out"
},
# runtime tests
{
"patch": "tests/test5.patch",
"cpp": "tests/test5.cpp",
"out": "tests/test5.out"
},
]
def main():
cwd = os.path.abspath('.')
scm = detect_scm_system(cwd)
cpplint.use_mozilla_styles()
(args, flags) = cpplint.parse_arguments([])
for test in TESTS:
with open(test["patch"]) as fh:
patch = fh.read()
with closing(StringIO.StringIO()) as output:
cpplint.set_stream(output)
checkmozstyle.process_patch(patch, cwd, cwd, scm)
result = output.getvalue()
with open(test["out"]) as fh:
expected_output = fh.read()
test_status = "PASSED"
if result != expected_output:
test_status = "FAILED"
print("TEST " + test["patch"] + " " + test_status)
print("Got result:\n" + result + "Expected:\n" + expected_output)
else:
print("TEST " + test["patch"] + " " + test_status)
if __name__ == "__main__":
main()

View file

View file

@ -0,0 +1 @@
patch:0: Patch does not appear to diff against any file. [patch/notempty] [3]

View file

@ -0,0 +1 @@
Bad patch that doesn't diff any files

View file

@ -0,0 +1,3 @@
int main() {
return 0;
}

View file

@ -0,0 +1,4 @@
patch:0: Patch does not have a summary. [patch/nosummary] [3]
patch:0: Patch does not have a description. [patch/nodescription] [3]
tests/test2.cpp:1: No copyright message found. [legal/copyright] [3]
Done processing tests/test2.cpp

View file

@ -0,0 +1,9 @@
# Test
diff --git a/tests/test2.cpp b/tests/test2.cpp
new file mode 100644
--- /dev/null
+++ b/tests/test2.cpp
@@ -0,0 +1,3 @@
+int main() {
+ return 0;
+}

View file

@ -0,0 +1,3 @@
patch:0: Patch summary should begin with 'Bug XXXXX - ' or 'No bug -'. [patch/bugnumber] [3]
tests/test2.cpp:1: No copyright message found. [legal/copyright] [3]
Done processing tests/test2.cpp

View file

@ -0,0 +1,12 @@
# Test
patch summary with no bug number
Some bogus patch description
diff --git a/tests/test2.cpp b/tests/test2.cpp
new file mode 100644
--- /dev/null
+++ b/tests/test2.cpp
@@ -0,0 +1,3 @@
+int main() {
+ return 0;
+}

View file

@ -0,0 +1,40 @@
class ShouldUseExplicit {
// runtime/explicit
ShouldUseExplicit(int i);
};
// readability/function
int foo(int) {
}
int main() {
int i = 0;
// readability/control_flow
// XXX This doesn't trigger it. It needs to be fixed.
if (i) {
return;
} else {
i++;
}
// whitespace/parens
if(i){}
// readability/casting
void* bad = (void*)i;
// readability/comparison_to_zero
if (i == true) {}
if (i == false) {}
if (i != true) {}
if (i != false) {}
if (i == NULL) {}
if (i != NULL) {}
if (i == nullptr) {}
if (i != nullptr) {}
if (i) {}
if (!i) {}
return 0;
}

View file

@ -0,0 +1,13 @@
tests/test4.cpp:1: No copyright message found. [legal/copyright] [3]
tests/test4.cpp:3: Single-argument constructors should be marked explicit. [runtime/explicit] [5]
tests/test4.cpp:7: All parameters should be named in a function [readability/function] [3]
tests/test4.cpp:22: Missing space before ( in if( [whitespace/parens] [5]
tests/test4.cpp:22: Missing space before { [whitespace/braces] [5]
tests/test4.cpp:25: Using C-style cast. Use reinterpret_cast<void*>(...) instead [readability/casting] [4]
tests/test4.cpp:28: Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons. [readability/comparison_to_zero] [5]
tests/test4.cpp:29: Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons. [readability/comparison_to_zero] [5]
tests/test4.cpp:30: Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons. [readability/comparison_to_zero] [5]
tests/test4.cpp:31: Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons. [readability/comparison_to_zero] [5]
tests/test4.cpp:32: Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons. [readability/comparison_to_zero] [5]
tests/test4.cpp:33: Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons. [readability/comparison_to_zero] [5]
Done processing tests/test4.cpp

View file

@ -0,0 +1,49 @@
# Test
Bug 12 - patch summary with no bug number
Some bogus patch description
diff --git a/tests/test4.cpp b/tests/test4.cpp
new file mode 100644
--- /dev/null
+++ b/tests/test4.cpp
@@ -0,0 +1,49 @@
+class ShouldUseExplicit {
+ // runtime/explicit
+ ShouldUseExplicit(int i);
+};
+
+// readability/function
+int foo(int) {
+}
+
+int main() {
+ int i = 0;
+
+ // readability/control_flow
+ // XXX This doesn't trigger it. It needs to be fixed.
+ if (i) {
+ return;
+ } else {
+ i++;
+ }
+
+ // whitespace/parens
+ if(i){}
+
+ // readability/casting
+ void* bad = (void*)i;
+
+ // readability/comparison_to_zero
+ if (i == true) {}
+ if (i == false) {}
+ if (i != true) {}
+ if (i != false) {}
+ if (i == NULL) {}
+ if (i != NULL) {}
+ if (i == nullptr) {}
+ if (i != nullptr) {}
+ if (i) {}
+ if (!i) {}
+
+ return 0;
+}

View file

@ -0,0 +1,24 @@
// License bogus
// runtime/virtual
class ShouldHaveVirtualDes {
virtual foo();
};
int main() {
// runtime/memset
memset(blah, sizeof(blah), 0);
// runtime/rtti
dynamic_cast<Derived*>(obj);
// runtime/sizeof
int varname = 0;
int mySize = sizeof(int);
// runtime/threadsafe_fn
getpwuid();
strtok();
return 0;
}

View file

@ -0,0 +1,7 @@
tests/test5.cpp:4: The class ShouldHaveVirtualDes probably needs a virtual destructor due to having virtual method(s), one declared at line 5. [runtime/virtual] [4]
tests/test5.cpp:10: Did you mean "memset(blah, 0, sizeof(blah))"? [runtime/memset] [4]
tests/test5.cpp:13: Do not use dynamic_cast<>. If you need to cast within a class hierarchy, use static_cast<> to upcast. Mozilla doesn't support RTTI. [runtime/rtti] [5]
tests/test5.cpp:17: Using sizeof(type). Use sizeof(varname) instead if possible [runtime/sizeof] [1]
tests/test5.cpp:20: Consider using getpwuid_r(...) instead of getpwuid(...) for improved thread safety. [runtime/threadsafe_fn] [2]
tests/test5.cpp:21: Consider using strtok_r(...) instead of strtok(...) for improved thread safety. [runtime/threadsafe_fn] [2]
Done processing tests/test5.cpp

View file

@ -0,0 +1,33 @@
# Test
Bug 12 - patch summary with no bug number
Some bogus patch description
diff --git a/tests/test5.cpp b/tests/test5.cpp
new file mode 100644
--- /dev/null
+++ b/tests/test5.cpp
@@ -0,0 +1,24 @@
+// License bogus
+
+// runtime/virtual
+class ShouldHaveVirtualDes {
+ virtual foo();
+};
+
+int main() {
+ // runtime/memset
+ memset(blah, sizeof(blah), 0);
+
+ // runtime/rtti
+ dynamic_cast<Derived*>(obj);
+
+ // runtime/sizeof
+ int varname = 0;
+ int mySize = sizeof(int);
+
+ // runtime/threadsafe_fn
+ getpwuid();
+ strtok();
+
+ return 0;
+}

128
tools/coverity/model.cpp Normal file
View file

@ -0,0 +1,128 @@
/*
Coverity model file in order to avoid false-positive
*/
#define NULL (void *)0
typedef unsigned char jsbytecode;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned int int32_t;
typedef unsigned char uint8_t;
static const uint16_t CHUNK_HEAD_SIZE = 8;
void assert(bool expr) {
if (!expr) {
__coverity_panic__();
}
}
#define ERREXIT(cinfo, err) __coverity_panic__();
void MOZ_ASSUME_UNREACHABLE(char * str) {
__coverity_panic__();
}
static void MOZ_ReportAssertionFailure(const char* aStr, const char* aFilename,
int aLine) {
__coverity_panic__();
}
static void MOZ_ReportCrash(const char* aStr, const char* aFilename,
int aLine) {
__coverity_panic__();
}
#define MOZ_ASSERT(expr, msg) assert(!!(expr))
#define MOZ_ASSERT(expr) assert(!!(expr))
#define NS_ASSERTION(expr, msg) assert(!!(expr))
#define PORT_Assert(expr) assert(!!(expr))
#define PR_ASSERT(expr) assert(!!(expr))
int GET_JUMP_OFFSET(jsbytecode* pc) {
__coverity_tainted_data_sanitize__(&pc[1]);
__coverity_tainted_data_sanitize__(&pc[2]);
__coverity_tainted_data_sanitize__(&pc[3]);
__coverity_tainted_data_sanitize__(&pc[4]);
return 0;
}
// Data sanity checkers
#define XPT_SWAB16(data) __coverity_tainted_data_sanitize__(&data)
#define XPT_SWAB32(data) __coverity_tainted_data_sanitize__(&data)
static unsigned GET_UINT24(const jsbytecode* pc) {
__coverity_tainted_data_sanitize__(static_cast<void*>(pc));
//return unsigned((pc[1] << 16) | (pc[2] << 8) | pc[3]);
return 0;
}
class HeaderParser {
private:
class ChunkHeader {
uint8_t mRaw[CHUNK_HEAD_SIZE];
HeaderParser::ChunkHeader::ChunkSize() const {
__coverity_tainted_data_sanitize__(static_cast<void*>(&mRaw[4]));
__coverity_tainted_data_sanitize__(static_cast<void*>(&mRaw[5]));
__coverity_tainted_data_sanitize__(static_cast<void*>(&mRaw[6]));
__coverity_tainted_data_sanitize__(static_cast<void*>(&mRaw[7]));
return ((mRaw[7] << 24) | (mRaw[6] << 16) | (mRaw[5] << 8) | (mRaw[4]));
}
};
};
void NS_DebugBreak(uint32_t aSeverity, const char* aStr, const char* aExpr,
const char* aFile, int32_t aLine) {
__coverity_panic__();
}
static inline void Swap(uint32_t* value) {
__coverity_tainted_data_sanitize__(static_cast<void*>(&value));
*value = (*value >> 24) |
((*value >> 8) & 0x0000ff00) |
((*value << 8) & 0x00ff0000) |
(*value << 24);
}
static uint32_t xtolong (const uint8_t *ll) {
__coverity_tainted_data_sanitize__(static_cast<void*>(&ll[0]));
__coverity_tainted_data_sanitize__(static_cast<void*>(&ll[1]));
__coverity_tainted_data_sanitize__(static_cast<void*>(&ll[2]));
__coverity_tainted_data_sanitize__(static_cast<void*>(&ll[3]));
return (uint32_t)( (ll [0] << 0) |
(ll [1] << 8) |
(ll [2] << 16) |
(ll [3] << 24) );
}
class ByteReader {
public:
const uint8_t* Read(size_t aCount);
uint32_t ReadU24() {
const uint8_t *ptr = Read(3);
if (!ptr) {
MOZ_ASSERT(false);
return 0;
}
__coverity_tainted_data_sanitize__(static_cast<void*>(&ptr[0]));
__coverity_tainted_data_sanitize__(static_cast<void*>(&ptr[1]));
__coverity_tainted_data_sanitize__(static_cast<void*>(&ptr[2]));
return ptr[0] << 16 | ptr[1] << 8 | ptr[2];
}
};

13
tools/docs/Vagrantfile vendored Normal file
View file

@ -0,0 +1,13 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# We intentionally use the old config format because Mozilla's Jenkins
# server doesn't run a modern Vagrant.
Vagrant::Config.run do |config|
config.vm.box = "precise64"
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
config.vm.share_folder("gecko", "/gecko", "../..")
# Doxygen needs more than the default memory or it will swap and be
# extremely slow.
config.vm.customize ["modifyvm", :id, "--memory", 2048]
end

83
tools/docs/conf.py Normal file
View file

@ -0,0 +1,83 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import unicode_literals
import os
import re
import sys
from datetime import datetime
# Set up Python environment to load build system packages.
OUR_DIR = os.path.dirname(__file__)
topsrcdir = os.path.normpath(os.path.join(OUR_DIR, '..', '..'))
EXTRA_PATHS = (
'layout/tools/reftest',
'python/futures',
'python/jsmin',
'python/mach',
'python/mozbuild',
'python/mozversioncontrol',
'python/which',
'testing/mozbase/manifestparser',
'testing/mozbase/mozfile',
'testing/mozbase/mozprocess',
)
sys.path[:0] = [os.path.join(topsrcdir, p) for p in EXTRA_PATHS]
sys.path.insert(0, OUR_DIR)
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.graphviz',
'sphinx.ext.todo',
'mozbuild.sphinx',
]
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
project = u'Mozilla Source Tree Docs'
year = datetime.now().year
# Grab the version from the source tree's milestone.
# FUTURE Use Python API from bug 941299.
with open(os.path.join(topsrcdir, 'config', 'milestone.txt'), 'rt') as fh:
for line in fh:
line = line.strip()
if not line or line.startswith('#'):
continue
release = line
break
version = re.sub(r'[ab]\d+$', '', release)
exclude_patterns = ['_build', '_staging', '_venv']
pygments_style = 'sphinx'
# We need to perform some adjustment of the settings and environment
# when running on Read The Docs.
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if on_rtd:
# SHELL isn't set on RTD and mach.mixin.process's import raises if a
# shell-related environment variable can't be found. Set the variable here
# to hack us into working on RTD.
assert 'SHELL' not in os.environ
os.environ['SHELL'] = '/bin/bash'
else:
# We only need to set the RTD theme when not on RTD because the RTD
# environment handles this otherwise.
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
html_static_path = ['_static']
htmlhelp_basename = 'MozillaTreeDocs'

58
tools/docs/index.rst Normal file
View file

@ -0,0 +1,58 @@
=================================
Mozilla Source Tree Documentation
=================================
.. toctree::
:maxdepth: 1
{indexes}
Python Packages
===============
.. toctree::
:maxdepth: 2
{python_packages}
Managing Documentation
======================
This documentation is generated via the
`Sphinx <http://sphinx-doc.org/>`_ tool from sources in the tree.
To build the documentation, run ``mach doc``. Run
``mach help doc`` to see configurable options.
Adding Documentation
--------------------
To add new documentation, define the ``SPHINX_TREES`` and
``SPHINX_PYTHON_PACKAGE_DIRS`` variables in ``moz.build`` files in
the tree and documentation will automatically get picked up.
Say you have a directory ``featureX`` you would like to write some
documentation for. Here are the steps to create Sphinx documentation
for it:
1. Create a directory for the docs. This is typically ``docs``. e.g.
``featureX/docs``.
2. Create an ``index.rst`` file in this directory. The ``index.rst`` file
is the root documentation for that section. See ``build/docs/index.rst``
for an example file.
3. In a ``moz.build`` file (typically the one in the parent directory of
the ``docs`` directory), define ``SPHINX_TREES`` to hook up the plumbing.
e.g. ``SPHINX_TREES['featureX'] = 'docs'``. This says *the ``docs``
directory under the current directory should be installed into the
Sphinx documentation tree under ``/featureX``*.
4. If you have Python packages you would like to generate Python API
documentation for, you can use ``SPHINX_PYTHON_PACKAGE_DIRS`` to
declare directories containing Python packages. e.g.
``SPHINX_PYTHON_PACKAGE_DIRS += ['mozpackage']``.
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

117
tools/docs/mach_commands.py Normal file
View file

@ -0,0 +1,117 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, # You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import, print_function, unicode_literals
import os
import sys
from mach.decorators import (
Command,
CommandArgument,
CommandProvider,
)
import mozhttpd
from mozbuild.base import MachCommandBase
@CommandProvider
class Documentation(MachCommandBase):
"""Helps manage in-tree documentation."""
@Command('doc', category='devenv',
description='Generate and display documentation from the tree.')
@CommandArgument('what', nargs='*', metavar='DIRECTORY [, DIRECTORY]',
help='Path(s) to documentation to build and display.')
@CommandArgument('--format', default='html',
help='Documentation format to write.')
@CommandArgument('--outdir', default=None, metavar='DESTINATION',
help='Where to write output.')
@CommandArgument('--no-open', dest='auto_open', default=True, action='store_false',
help="Don't automatically open HTML docs in a browser.")
@CommandArgument('--http', const=':6666', metavar='ADDRESS', nargs='?',
help='Serve documentation on an HTTP server, e.g. ":6666".')
def build_docs(self, what=None, format=None, outdir=None, auto_open=True, http=None):
self._activate_virtualenv()
self.virtualenv_manager.install_pip_package('sphinx_rtd_theme==0.1.6')
import sphinx
import webbrowser
if not outdir:
outdir = os.path.join(self.topobjdir, 'docs')
if not what:
what = [os.path.join(self.topsrcdir, 'tools')]
outdir = os.path.join(outdir, format)
generated = []
failed = []
for path in what:
path = os.path.normpath(os.path.abspath(path))
docdir = self._find_doc_dir(path)
if not docdir:
failed.append((path, 'could not find docs at this location'))
continue
# find project name to use as a namespace within `outdir`
project = self._find_project_name(docdir)
savedir = os.path.join(outdir, project)
args = [
'sphinx',
'-b', format,
docdir,
savedir,
]
result = sphinx.build_main(args)
if result != 0:
failed.append((path, 'sphinx return code %d' % result))
else:
generated.append(savedir)
index_path = os.path.join(savedir, 'index.html')
if not http and auto_open and os.path.isfile(index_path):
webbrowser.open(index_path)
if generated:
print('\nGenerated documentation:\n%s\n' % '\n'.join(generated))
if failed:
failed = ['%s: %s' % (f[0], f[1]) for f in failed]
return die('failed to generate documentation:\n%s' % '\n'.join(failed))
if http is not None:
host, port = http.split(':', 1)
addr = (host, int(port))
if len(addr) != 2:
return die('invalid address: %s' % http)
httpd = mozhttpd.MozHttpd(host=addr[0], port=addr[1], docroot=outdir)
print('listening on %s:%d' % addr)
httpd.start(block=True)
def _find_project_name(self, path):
import imp
path = os.path.join(path, 'conf.py')
with open(path, 'r') as fh:
conf = imp.load_module('doc_conf', fh, path,
('.py', 'r', imp.PY_SOURCE))
return conf.project.replace(' ', '_')
def _find_doc_dir(self, path):
search_dirs = ('doc', 'docs')
for d in search_dirs:
p = os.path.join(path, d)
if os.path.isfile(os.path.join(p, 'conf.py')):
return p
def die(msg, exit_code=1):
msg = '%s: %s' % (sys.argv[0], msg)
print(msg, file=sys.stderr)
return exit_code

View file

@ -0,0 +1,126 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, # You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import unicode_literals
import os
from mozbuild.frontend.reader import BuildReader
from mozpack.copier import FileCopier
from mozpack.files import FileFinder
from mozpack.manifests import InstallManifest
import sphinx
import sphinx.apidoc
class SphinxManager(object):
"""Manages the generation of Sphinx documentation for the tree."""
def __init__(self, topsrcdir, main_path, output_dir):
self._topsrcdir = topsrcdir
self._output_dir = output_dir
self._docs_dir = os.path.join(output_dir, '_staging')
self._conf_py_path = os.path.join(main_path, 'conf.py')
self._index_path = os.path.join(main_path, 'index.rst')
self._trees = {}
self._python_package_dirs = set()
def read_build_config(self):
"""Read the active build config and add docs to this instance."""
# Reading the Sphinx variables doesn't require a full build context.
# Only define the parts we need.
class fakeconfig(object):
def __init__(self, topsrcdir):
self.topsrcdir = topsrcdir
config = fakeconfig(self._topsrcdir)
reader = BuildReader(config)
for path, name, key, value in reader.find_sphinx_variables():
reldir = os.path.dirname(path)
if name == 'SPHINX_TREES':
assert key
self.add_tree(os.path.join(reldir, value),
os.path.join(reldir, key))
if name == 'SPHINX_PYTHON_PACKAGE_DIRS':
self.add_python_package_dir(os.path.join(reldir, value))
def add_tree(self, source_dir, dest_dir):
"""Add a directory from where docs should be sourced."""
if dest_dir in self._trees:
raise Exception('%s has already been registered as a destination.'
% dest_dir)
self._trees[dest_dir] = source_dir
def add_python_package_dir(self, source_dir):
"""Add a directory containing Python packages.
Added directories will have Python API docs generated automatically.
"""
self._python_package_dirs.add(source_dir)
def generate_docs(self, app):
"""Generate/stage documentation."""
app.info('Reading Sphinx metadata from build configuration')
self.read_build_config()
app.info('Staging static documentation')
self._synchronize_docs()
app.info('Generating Python API documentation')
self._generate_python_api_docs()
def _generate_python_api_docs(self):
"""Generate Python API doc files."""
out_dir = os.path.join(self._docs_dir, 'python')
base_args = ['sphinx', '--no-toc', '-o', out_dir]
for p in sorted(self._python_package_dirs):
full = os.path.join(self._topsrcdir, p)
finder = FileFinder(full, find_executables=False)
dirs = {os.path.dirname(f[0]) for f in finder.find('**')}
excludes = {d for d in dirs if d.endswith('test')}
args = list(base_args)
args.append(full)
args.extend(excludes)
sphinx.apidoc.main(args)
def _synchronize_docs(self):
m = InstallManifest()
m.add_symlink(self._conf_py_path, 'conf.py')
for dest, source in sorted(self._trees.items()):
source_dir = os.path.join(self._topsrcdir, source)
for root, dirs, files in os.walk(source_dir):
for f in files:
source_path = os.path.join(root, f)
rel_source = source_path[len(source_dir) + 1:]
m.add_symlink(source_path, os.path.join(dest, rel_source))
copier = FileCopier()
m.populate_registry(copier)
copier.copy(self._docs_dir)
with open(self._index_path, 'rb') as fh:
data = fh.read()
indexes = ['%s/index' % p for p in sorted(self._trees.keys())]
indexes = '\n '.join(indexes)
packages = [os.path.basename(p) for p in self._python_package_dirs]
packages = ['python/%s' % p for p in packages]
packages = '\n '.join(sorted(packages))
data = data.format(indexes=indexes, python_packages=packages)
with open(os.path.join(self._docs_dir, 'index.rst'), 'wb') as fh:
fh.write(data)

View file

@ -0,0 +1,67 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Interface implementation for the unified fuzzing interface
*/
#include "FuzzingInterface.h"
#include "nsNetUtil.h"
namespace mozilla {
#ifdef __AFL_COMPILER
void afl_interface_stream(const char* testFile, FuzzingTestFuncStream testFunc) {
nsresult rv;
nsCOMPtr<nsIProperties> dirService =
do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
MOZ_RELEASE_ASSERT(dirService != nullptr);
nsCOMPtr<nsIFile> file;
rv = dirService->Get(NS_OS_CURRENT_WORKING_DIR,
NS_GET_IID(nsIFile), getter_AddRefs(file));
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
file->AppendNative(nsDependentCString(testFile));
while(__AFL_LOOP(1000)) {
nsCOMPtr<nsIInputStream> inputStream;
rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), file);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
if (!NS_InputStreamIsBuffered(inputStream)) {
nsCOMPtr<nsIInputStream> bufStream;
rv = NS_NewBufferedInputStream(getter_AddRefs(bufStream),
inputStream, 1024);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
inputStream = bufStream;
}
testFunc(inputStream.forget());
}
}
void afl_interface_raw(const char* testFile, FuzzingTestFuncRaw testFunc) {
char* buf = NULL;
while(__AFL_LOOP(1000)) {
std::ifstream is;
is.open (testFile, std::ios::binary);
is.seekg (0, std::ios::end);
int len = is.tellg();
is.seekg (0, std::ios::beg);
MOZ_RELEASE_ASSERT(len >= 0);
if (!len) {
is.close();
continue;
}
buf = (char*)realloc(buf, len);
MOZ_RELEASE_ASSERT(buf);
is.read(buf,len);
is.close();
testFunc((uint8_t*)buf, (size_t)len);
}
free(buf);
}
#endif
} // namespace mozilla

View file

@ -0,0 +1,100 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Interface definitions for the unified fuzzing interface
*/
#ifndef FuzzingInterface_h__
#define FuzzingInterface_h__
#include "gtest/gtest.h"
#include "nsComponentManagerUtils.h"
#include "nsCOMPtr.h"
#include "nsIInputStream.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIDirectoryService.h"
#include "nsIFile.h"
#include "nsStreamUtils.h"
#include "nsStringStream.h"
#include <fstream>
namespace mozilla {
typedef int(*FuzzingTestFuncRaw)(const uint8_t*, size_t);
typedef int(*FuzzingTestFuncStream)(nsCOMPtr<nsIInputStream>);
#ifdef __AFL_COMPILER
void afl_interface_stream(const char* testFile, FuzzingTestFuncStream testFunc);
void afl_interface_raw(const char* testFile, FuzzingTestFuncRaw testFunc);
#define MOZ_AFL_INTERFACE_COMMON(initFunc) \
initFunc(NULL, NULL); \
char* testFilePtr = getenv("MOZ_FUZZ_TESTFILE"); \
if (!testFilePtr) { \
EXPECT_TRUE(false) << "Must specify testfile in MOZ_FUZZ_TESTFILE environment variable."; \
return; \
} \
/* Make a copy of testFilePtr so the testing function can safely call getenv */ \
std::string testFile(testFilePtr);
#define MOZ_AFL_INTERFACE_STREAM(initFunc, testFunc, moduleName) \
TEST(AFL, moduleName) { \
MOZ_AFL_INTERFACE_COMMON(initFunc); \
::mozilla::afl_interface_stream(testFile.c_str(), testFunc); \
}
#define MOZ_AFL_INTERFACE_RAW(initFunc, testFunc, moduleName) \
TEST(AFL, moduleName) { \
MOZ_AFL_INTERFACE_COMMON(initFunc); \
::mozilla::afl_interface_raw(testFile.c_str(), testFunc); \
}
#else
#define MOZ_AFL_INTERFACE_STREAM(initFunc, testFunc, moduleName) /* Nothing */
#define MOZ_AFL_INTERFACE_RAW(initFunc, testFunc, moduleName) /* Nothing */
#endif
#ifdef LIBFUZZER
#define MOZ_LIBFUZZER_INTERFACE_STREAM(initFunc, testFunc, moduleName) \
static int LibFuzzerTest##moduleName (const uint8_t *data, size_t size) { \
if (size > INT32_MAX) \
return 0; \
nsCOMPtr<nsIInputStream> stream; \
nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream), \
(const char*)data, size, NS_ASSIGNMENT_DEPEND); \
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); \
testFunc(stream.forget()); \
return 0; \
} \
static void __attribute__ ((constructor)) LibFuzzerRegister() { \
::mozilla::LibFuzzerRegistry::getInstance().registerModule( \
#moduleName, initFunc, LibFuzzerTest##moduleName \
); \
}
#define MOZ_LIBFUZZER_INTERFACE_RAW(initFunc, testFunc, moduleName) \
static void __attribute__ ((constructor)) LibFuzzerRegister() { \
::mozilla::LibFuzzerRegistry::getInstance().registerModule( \
#moduleName, initFunc, testFunc \
); \
}
#else
#define MOZ_LIBFUZZER_INTERFACE_STREAM(initFunc, testFunc, moduleName) /* Nothing */
#define MOZ_LIBFUZZER_INTERFACE_RAW(initFunc, testFunc, moduleName) /* Nothing */
#endif
#define MOZ_FUZZING_INTERFACE_STREAM(initFunc, testFunc, moduleName) \
MOZ_LIBFUZZER_INTERFACE_STREAM(initFunc, testFunc, moduleName); \
MOZ_AFL_INTERFACE_STREAM(initFunc, testFunc, moduleName);
#define MOZ_FUZZING_INTERFACE_RAW(initFunc, testFunc, moduleName) \
MOZ_LIBFUZZER_INTERFACE_RAW(initFunc, testFunc, moduleName); \
MOZ_AFL_INTERFACE_RAW(initFunc, testFunc, moduleName);
} // namespace mozilla
#endif // FuzzingInterface_h__

View file

@ -0,0 +1,15 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXPORTS += [
'FuzzingInterface.h',
]
SOURCES += [
'FuzzingInterface.cpp',
]
FINAL_LIBRARY = 'xul-gtest'

View file

@ -0,0 +1,37 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* * This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <cstdlib>
#include "FuzzerInterface.h"
#include "FuzzerInternal.h"
#include "harness/LibFuzzerRegistry.h"
/* This is a wrapper defined in browser/app/nsBrowserApp.cpp,
* encapsulating the XRE_ equivalent defined in libxul */
extern void libFuzzerGetFuncs(const char*, LibFuzzerInitFunc*,
LibFuzzerTestingFunc*);
int libfuzzer_main(int argc, char **argv) {
LibFuzzerInitFunc initFunc = nullptr;
LibFuzzerTestingFunc testingFunc = nullptr;
libFuzzerGetFuncs(getenv("LIBFUZZER"), &initFunc, &testingFunc);
if (initFunc) {
int ret = initFunc(&argc, &argv);
if (ret) {
fprintf(stderr, "LibFuzzer: Error: Initialize callback failed\n");
return ret;
}
}
if (!testingFunc) {
fprintf(stderr, "LibFuzzer: Error: No testing callback found\n");
return 1;
}
return fuzzer::FuzzerDriver(&argc, &argv, testingFunc);
}

View file

@ -0,0 +1,12 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
include $(topsrcdir)/config/rules.mk
# According to the LLVM docs, LibFuzzer isn't supposed to be built with any
# sanitizer flags and in fact, building it with ASan coverage currently causes
# Clang 3.9+ to crash, so we filter out all sanitizer-related flags here.
CXXFLAGS := $(filter-out -fsanitize%,$(CXXFLAGS))
CFLAGS := $(filter-out -fsanitize%,$(CFLAGS))
LDFLAGS := $(filter-out -fsanitize%,$(LDFLAGS))

View file

@ -0,0 +1,7 @@
#!/bin/sh
mkdir tmp/
git clone --no-checkout --depth 1 https://chromium.googlesource.com/chromium/llvm-project/llvm/lib/Fuzzer tmp/
mv tmp/.git .
rm -Rf tmp
git reset --hard HEAD

View file

@ -0,0 +1,32 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* * This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "LibFuzzerRegistry.h"
extern "C" {
void MOZ_EXPORT XRE_LibFuzzerGetFuncs(const char* moduleName, LibFuzzerInitFunc* initFunc, LibFuzzerTestingFunc* testingFunc) {
std::string moduleNameStr(moduleName);
mozilla::LibFuzzerFunctions funcs = mozilla::LibFuzzerRegistry::getInstance().getModuleFunctions(moduleNameStr);
*initFunc = funcs.first;
*testingFunc = funcs.second;
}
}
namespace mozilla {
LibFuzzerRegistry& LibFuzzerRegistry::getInstance() {
static LibFuzzerRegistry instance;
return instance;
}
void LibFuzzerRegistry::registerModule(std::string moduleName, LibFuzzerInitFunc initFunc, LibFuzzerTestingFunc testingFunc) {
moduleMap.insert(std::pair<std::string, LibFuzzerFunctions>(moduleName,LibFuzzerFunctions(initFunc, testingFunc)));
}
LibFuzzerFunctions LibFuzzerRegistry::getModuleFunctions(std::string& moduleName) {
return moduleMap[moduleName];
}
} // namespace mozilla

View file

@ -0,0 +1,41 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* * This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef _LibFuzzerRegistry_h__
#define _LibFuzzerRegistry_h__
#include <cstdint>
#include <map>
#include <string>
#include <utility>
#include "mozilla/Attributes.h"
typedef int(*LibFuzzerMain)(int, char**);
typedef int(*LibFuzzerInitFunc)(int*, char***);
typedef int(*LibFuzzerTestingFunc)(const uint8_t*, size_t);
namespace mozilla {
typedef std::pair<LibFuzzerInitFunc, LibFuzzerTestingFunc> LibFuzzerFunctions;
class LibFuzzerRegistry {
public:
MOZ_EXPORT static LibFuzzerRegistry& getInstance();
MOZ_EXPORT void registerModule(std::string moduleName, LibFuzzerInitFunc initFunc, LibFuzzerTestingFunc testingFunc);
MOZ_EXPORT LibFuzzerFunctions getModuleFunctions(std::string& moduleName);
LibFuzzerRegistry(LibFuzzerRegistry const&) = delete;
void operator=(LibFuzzerRegistry const&) = delete;
private:
LibFuzzerRegistry() {};
std::map<std::string, LibFuzzerFunctions> moduleMap;
};
} // namespace mozilla
#endif // _LibFuzzerRegistry_h__

View file

@ -0,0 +1,38 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* * This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "LibFuzzerRunner.h"
#include "mozilla/Attributes.h"
#include "prenv.h"
#include "LibFuzzerTestHarness.h"
namespace mozilla {
// We use a static var 'libFuzzerRunner' defined in nsAppRunner.cpp.
// libFuzzerRunner is initialized to nullptr but if LibFuzzer (this file)
// is linked in then libFuzzerRunner will be set here indicating that
// we want to call into LibFuzzer's main.
class _InitLibFuzzer {
public:
_InitLibFuzzer() {
libFuzzerRunner = new LibFuzzerRunner();
}
} InitLibFuzzer;
int LibFuzzerRunner::Run() {
ScopedXPCOM xpcom("LibFuzzer");
return mFuzzerMain(mArgc, mArgv);
}
typedef int(*LibFuzzerMain)(int, char**);
void LibFuzzerRunner::setParams(int argc, char** argv, LibFuzzerMain main) {
mArgc = argc;
mArgv = argv;
mFuzzerMain = main;
}
} // namespace mozilla

View file

@ -0,0 +1,23 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* * This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
namespace mozilla {
typedef int(*LibFuzzerMain)(int, char**);
class LibFuzzerRunner {
public:
int Run();
void setParams(int argc, char** argv, LibFuzzerMain main);
private:
int mArgc;
char** mArgv;
LibFuzzerMain mFuzzerMain;
};
extern LibFuzzerRunner* libFuzzerRunner;
} // namespace mozilla

View file

@ -0,0 +1,298 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Test harness for XPCOM objects, providing a scoped XPCOM initializer,
* nsCOMPtr, nsRefPtr, do_CreateInstance, do_GetService, ns(Auto|C|)String,
* and stdio.h/stdlib.h.
*/
#ifndef LibFuzzerTestHarness_h__
#define LibFuzzerTestHarness_h__
#include "mozilla/ArrayUtils.h"
#include "prenv.h"
#include "nsComponentManagerUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsCOMPtr.h"
#include "nsAutoPtr.h"
#include "nsStringGlue.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIDirectoryService.h"
#include "nsIFile.h"
#include "nsIProperties.h"
#include "nsIObserverService.h"
#include "nsXULAppAPI.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
namespace {
static uint32_t gFailCount = 0;
/**
* Prints the given failure message and arguments using printf, prepending
* "TEST-UNEXPECTED-FAIL " for the benefit of the test harness and
* appending "\n" to eliminate having to type it at each call site.
*/
void fail(const char* msg, ...)
{
va_list ap;
printf("TEST-UNEXPECTED-FAIL | ");
va_start(ap, msg);
vprintf(msg, ap);
va_end(ap);
putchar('\n');
++gFailCount;
}
/**
* Prints the given success message and arguments using printf, prepending
* "TEST-PASS " for the benefit of the test harness and
* appending "\n" to eliminate having to type it at each call site.
*/
void passed(const char* msg, ...)
{
va_list ap;
printf("TEST-PASS | ");
va_start(ap, msg);
vprintf(msg, ap);
va_end(ap);
putchar('\n');
}
//-----------------------------------------------------------------------------
static class ScopedXPCOM : public nsIDirectoryServiceProvider2
{
public:
NS_DECL_ISUPPORTS
explicit ScopedXPCOM(const char* testName,
nsIDirectoryServiceProvider *dirSvcProvider = nullptr)
: mDirSvcProvider(dirSvcProvider)
{
mTestName = testName;
printf("Running %s tests...\n", mTestName);
nsresult rv = NS_InitXPCOM2(&mServMgr, nullptr, this);
if (NS_FAILED(rv))
{
fail("NS_InitXPCOM2 returned failure code 0x%x", rv);
mServMgr = nullptr;
return;
}
}
~ScopedXPCOM()
{
// If we created a profile directory, we need to remove it.
if (mProfD) {
nsCOMPtr<nsIObserverService> os =
do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
MOZ_ASSERT(os);
if (os) {
MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-change-net-teardown", nullptr));
MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-change-teardown", nullptr));
MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-before-change", nullptr));
MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-before-change-qm", nullptr));
MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-before-change-telemetry", nullptr));
}
if (NS_FAILED(mProfD->Remove(true))) {
NS_WARNING("Problem removing profile directory");
}
mProfD = nullptr;
}
if (mServMgr)
{
NS_RELEASE(mServMgr);
nsresult rv = NS_ShutdownXPCOM(nullptr);
if (NS_FAILED(rv))
{
fail("XPCOM shutdown failed with code 0x%x", rv);
exit(1);
}
}
printf("Finished running %s tests.\n", mTestName);
}
bool failed()
{
return mServMgr == nullptr;
}
already_AddRefed<nsIFile> GetProfileDirectory()
{
if (mProfD) {
nsCOMPtr<nsIFile> copy = mProfD;
return copy.forget();
}
// Create a unique temporary folder to use for this test.
// Note that runcppunittests.py will run tests with a temp
// directory as the cwd, so just put something under that.
nsCOMPtr<nsIFile> profD;
nsresult rv = NS_GetSpecialDirectory(NS_OS_CURRENT_PROCESS_DIR,
getter_AddRefs(profD));
NS_ENSURE_SUCCESS(rv, nullptr);
rv = profD->Append(NS_LITERAL_STRING("cpp-unit-profd"));
NS_ENSURE_SUCCESS(rv, nullptr);
rv = profD->CreateUnique(nsIFile::DIRECTORY_TYPE, 0755);
NS_ENSURE_SUCCESS(rv, nullptr);
mProfD = profD;
return profD.forget();
}
already_AddRefed<nsIFile> GetGREDirectory()
{
if (mGRED) {
nsCOMPtr<nsIFile> copy = mGRED;
return copy.forget();
}
char* env = PR_GetEnv("MOZ_XRE_DIR");
nsCOMPtr<nsIFile> greD;
if (env) {
NS_NewLocalFile(NS_ConvertUTF8toUTF16(env), false,
getter_AddRefs(greD));
}
mGRED = greD;
return greD.forget();
}
already_AddRefed<nsIFile> GetGREBinDirectory()
{
if (mGREBinD) {
nsCOMPtr<nsIFile> copy = mGREBinD;
return copy.forget();
}
nsCOMPtr<nsIFile> greD = GetGREDirectory();
if (!greD) {
return greD.forget();
}
greD->Clone(getter_AddRefs(mGREBinD));
#ifdef XP_MACOSX
nsAutoCString leafName;
mGREBinD->GetNativeLeafName(leafName);
if (leafName.Equals("Resources")) {
mGREBinD->SetNativeLeafName(NS_LITERAL_CSTRING("MacOS"));
}
#endif
nsCOMPtr<nsIFile> copy = mGREBinD;
return copy.forget();
}
////////////////////////////////////////////////////////////////////////////
//// nsIDirectoryServiceProvider
NS_IMETHODIMP GetFile(const char *aProperty, bool *_persistent,
nsIFile **_result) override
{
// If we were supplied a directory service provider, ask it first.
if (mDirSvcProvider &&
NS_SUCCEEDED(mDirSvcProvider->GetFile(aProperty, _persistent,
_result))) {
return NS_OK;
}
// Otherwise, the test harness provides some directories automatically.
if (0 == strcmp(aProperty, NS_APP_USER_PROFILE_50_DIR) ||
0 == strcmp(aProperty, NS_APP_USER_PROFILE_LOCAL_50_DIR) ||
0 == strcmp(aProperty, NS_APP_PROFILE_LOCAL_DIR_STARTUP)) {
nsCOMPtr<nsIFile> profD = GetProfileDirectory();
NS_ENSURE_TRUE(profD, NS_ERROR_FAILURE);
nsCOMPtr<nsIFile> clone;
nsresult rv = profD->Clone(getter_AddRefs(clone));
NS_ENSURE_SUCCESS(rv, rv);
*_persistent = true;
clone.forget(_result);
return NS_OK;
} else if (0 == strcmp(aProperty, NS_GRE_DIR)) {
nsCOMPtr<nsIFile> greD = GetGREDirectory();
NS_ENSURE_TRUE(greD, NS_ERROR_FAILURE);
*_persistent = true;
greD.forget(_result);
return NS_OK;
} else if (0 == strcmp(aProperty, NS_GRE_BIN_DIR)) {
nsCOMPtr<nsIFile> greBinD = GetGREBinDirectory();
NS_ENSURE_TRUE(greBinD, NS_ERROR_FAILURE);
*_persistent = true;
greBinD.forget(_result);
return NS_OK;
}
return NS_ERROR_FAILURE;
}
////////////////////////////////////////////////////////////////////////////
//// nsIDirectoryServiceProvider2
NS_IMETHODIMP GetFiles(const char *aProperty, nsISimpleEnumerator **_enum) override
{
// If we were supplied a directory service provider, ask it first.
nsCOMPtr<nsIDirectoryServiceProvider2> provider =
do_QueryInterface(mDirSvcProvider);
if (provider && NS_SUCCEEDED(provider->GetFiles(aProperty, _enum))) {
return NS_OK;
}
return NS_ERROR_FAILURE;
}
private:
const char* mTestName;
nsIServiceManager* mServMgr;
nsCOMPtr<nsIDirectoryServiceProvider> mDirSvcProvider;
nsCOMPtr<nsIFile> mProfD;
nsCOMPtr<nsIFile> mGRED;
nsCOMPtr<nsIFile> mGREBinD;
};
NS_IMPL_QUERY_INTERFACE(
ScopedXPCOM,
nsIDirectoryServiceProvider,
nsIDirectoryServiceProvider2
)
NS_IMETHODIMP_(MozExternalRefCountType)
ScopedXPCOM::AddRef()
{
return 2;
}
NS_IMETHODIMP_(MozExternalRefCountType)
ScopedXPCOM::Release()
{
return 1;
}
} // namespace
#endif // LibFuzzerTestHarness_h__

View file

@ -0,0 +1,19 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
Library('fuzzer-runner')
SOURCES += [
'LibFuzzerRegistry.cpp',
'LibFuzzerRunner.cpp',
]
EXPORTS += [
'LibFuzzerRegistry.h',
'LibFuzzerRunner.h',
]
FINAL_LIBRARY = "xul"

View file

@ -0,0 +1,26 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
Library('fuzzer')
DIRS += [
'harness',
]
SOURCES += [
'FuzzerCrossOver.cpp',
'FuzzerCustomMain.cpp',
'FuzzerDriver.cpp',
'FuzzerExtFunctionsDlsym.cpp',
'FuzzerExtFunctionsWeak.cpp',
'FuzzerIO.cpp',
'FuzzerLoop.cpp',
'FuzzerMutate.cpp',
'FuzzerSHA1.cpp',
'FuzzerTracePC.cpp',
'FuzzerTraceState.cpp',
'FuzzerUtil.cpp',
]

14
tools/fuzzing/moz.build Normal file
View file

@ -0,0 +1,14 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'interface',
]
if CONFIG['LIBFUZZER']:
DIRS += [
'libfuzzer',
]

330
tools/jprof/README.html Normal file
View file

@ -0,0 +1,330 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html>
<head><title>The Jprof Profiler</title></head>
<body bgcolor="#FFFFFF" text="#000000"
link="#0000EE" vlink="#551A8B" alink="#FF0000">
<center>
<h1>The Jprof Profiler</h1>
<font size="-1">
<a href="mailto:jim_nance%yahoo.com">jim_nance@yahoo.com</a><p>
Recent (4/2011) updates Randell Jesup (see bugzilla for contact info)
</font>
<hr>
<a href="#introduction">Introduction</a> | <a href="#operation">Operation</a> |
<a href="#setup">Setup</a> | <a href="#usage">Usage</a> |
<a href="#interpretation">Interpretation</a>
</center>
<hr>
<h3><a name="introduction">Introduction</a></h3>
Jprof is a profiling tool. I am writing it because I need to find out
where mozilla is spending its time, and there do not seem to be any
profilers for Linux that can handle threads and/or shared libraries.
This code is based heavily on Kipp Hickman's leaky.
<h3><a name="operation">Operation</a></h3>
Jprof operates by installing a timer which periodically interrupts mozilla.
When this timer goes off, the jprof code inside mozilla walks the function call
stack to determine which code was executing and saves the results into the
<code>jprof-log</code> and <code>jprof-map</code> files. By collecting a large
number of these call stacks, it is possible to deduce where mozilla is spending
its time.
<h3><a name="setup">Setup</a></h3>
<p>Configure your mozilla with jprof support by adding
<code>--enable-jprof</code> to your configure options (eg adding
<code>ac_add_options --enable-jprof</code> to your <code>.mozconfig</code>) and
making sure that you do <strong>not</strong> have the
<code>--enable-strip</code> configure option set -- jprof needs symbols to
operate. On many architectures with GCC, you'll need to add
<code>--enable-optimize="-O3 -fno-omit-frame-pointer"</code> or the
equivalent to ensure frame pointer generation in the compiler you're using.</p>
<p>Finally, build mozilla with your new configuration. Now you can run jprof.</p>
<h3><a name="usage">Usage</a></h3>
<pre> jprof [-v] [-t] [-e exclude] [-i include] [-s stackdepth] [--last] [--all] [--start n [--end m]] [--output-dir dir] prog log [log2 ...]</pre>
Options:
<ul>
<li><b>-s depth</b> : Limit depth looked at from captured stack
frames</li>
<li><b>-v</b> : Output some information about the symbols, memory map, etc.</li>
<li><b>-t or --threads</b> : Group output according to thread. May require external
LD_PRELOAD library to help force sampling of spawned threads; jprof
may capture the main thread only. See <a
href="http://sam.zoy.org/writings/programming/gprof.html">gprof-helper</a>;
it may need adaption for jprof.</li>
<li><b>--only-thread id</b> : Only output data for thread 'id'</li>
<li><b>-e exclusion</b> : Allows excluding specific stack frames</li>
<li><b>-i inclusion</b> : Allows including specific stack frames</li>
<li><b>--last</b> : Only process data from the last 'section' of sampling
(starting at the last PROF)</li>
<li><b>--start N</b> : Start processing data at 'section' N </li>
<li><b>--end N</b> : Stop processing data at 'section' N </li>
<li><b>--output-dir dir</b> : Store generated .html files in the given directory </li>
</ul>
The behavior of jprof is determined by the value of the JPROF_FLAGS environment
variable. This environment variable can be composed of several substrings
which have the following meanings:
<ul>
<li> <b>JP_START</b> : Install the signal handler, and start sending the
timer signals.
<li> <b>JP_DEFER</b> : Install the signal handler, but don't start sending
the timer signals. The user must start the signals by sending the first
one (with <code>kill -PROF</code>, or with <code>kill -ALRM</code> if
JP_REALTIME is used, or with <code>kill -POLL</code> (also known as <code>kill -IO</code>) if JP_RTC_HZ is used).
<li> <b>JP_FIRST=x</b> : Wait x seconds before starting the timer
<li> <b>JP_PERIOD=y</b> : Set timer to interrupt every y seconds. Only
values of y greater than or equal to 0.001 are supported. Default is
0.050 (50ms).
<li> <b>JP_REALTIME</b> : Do the profiling in intervals of real time rather
than intervals of time used by the mozilla process (and the kernel
when doing work for mozilla). This could probably lead to weird
results (you'll see whatever runs when mozilla is waiting for events),
but is needed to see time spent in the X server.
<li> <b>JP_RTC_HZ=freq</b> : This option, only available on Linux if the
kernel is built with RTC support, makes jprof use the RTC timer instead of
using its own timer. This option, like JP_REALTIME, uses intervals of real
time. This option overrides JP_PERIOD. <code>freq</code> is the frequency
at which the timer should fire, measured in Hz. It must be a power of 2.
The maximal frequency allowed by the kernel can be changed by writing to
<code>/proc/sys/dev/rtc/max-user-freq</code>; the maximum value it can be
set to is 8192. Note that <code>/dev/rtc</code> will need to be readable
by the Firefox process; making that file world-readable is a simple way to
accomplish that.
<li> <b>JP_CIRCULAR=size</b> : This tells jprof to store samples in a
circular buffer of the given size, which then will be saved (appended)
to disk when SIGUSR1 is received or JProfStopProfiling is done. If the
buffer overflows, the oldest entries will be evicted until there's
space for the new entry.<p>
SIGUSR2 will cause the circular buffer to be cleared.
<li> <b>JP_FILENAME=basefilename</b> : This is the filename used for
saving the log files to; the default is "jprof-log". If Electrolysis
is used, each process after the first will have the process ID
added ("jprof-log-3212");
</ul>
<h4>Starting and stopping jprof from JavaScript</h4>
<p>
A build with jprof enabled adds four functions to the Window object:<p>
<code>JProfStartProfiling()</code> and <code>JProfStopProfiling()</code>: When used with JP_DEFER, these
allow one to start and stop the timer just around whatever critical section is
being profiled.</p><p>
<code>JProfClearCircular()</code> and <code>JProfSaveCircular()</code>:
These clear the circular buffer and save the buffer (without stopping), respectively.</p>
<h4>Examples of JPROF_FLAGS usage</h4>
<ul>
<li>To make the timer start firing 3 seconds after the program is started and
fire every 25 milliseconds of program time use:
<pre>
setenv JPROF_FLAGS "JP_START JP_FIRST=3 JP_PERIOD=0.025" </pre>
<li>To make the timer start on your signal and fire every 1 millisecond of
program time use:
<pre>
setenv JPROF_FLAGS "JP_DEFER JP_PERIOD=0.001" </pre>
<li>To make the timer start on your signal and fire every 10 milliseconds of
wall-clock time use:
<pre>
setenv JPROF_FLAGS "JP_DEFER JP_PERIOD=0.010 JP_REALTIME" </pre>
<li>To make the timer start on your signal and fire at 8192 Hz in wall-clock
time use:
<pre>
setenv JPROF_FLAGS "JP_DEFER JP_RTC_HZ=8192" </pre>
<li>To make the timer start on JProfStartProfiling() and run continously
with a 1ms sample rate until told to stop, then save the last 1MB of
data:
<pre>
setenv JPROF_FLAGS "JP_DEFER JP_CIRCULAR=1048576 JP_PERIOD=0.001" </pre>
</ul>
<h4>Pausing profiles</h4>
<P>jprof can be paused at any time by sending a SIGUSR1 to mozilla (<code>kill
-USR1</code>). This will cause the timer signals to stop and jprof-map to be
written, but it will not close jprof-log. Combining SIGUSR1 with the JP_DEFER
option allows profiling of one sequence of actions by starting the timer right
before starting the actions and stopping the timer right afterward.
<P>After a SIGUSR1, sending another timer signal (SIGPROF, SIGALRM, or SIGPOLL (aka SIGIO),
depending on the mode) can be used to continue writing data to the same
output.
<P>SIGUSR2 will cause the circular buffer to be cleared, if it's in use.
This is useful right before running a test when you're using a large,
continuous circular buffer, or programmatically at the start of an action
which might take too long (JProfClearCircular()).
<h4>Looking at the results</h4>
Now that we have <code>jprof-log</code> and <code>jprof-map</code> files, we
can use the jprof executable is used to turn them into readable output. To do
this jprof needs the name of the mozilla binary and the log file. It deduces
the name of the map file:
<pre>
./jprof /home/user/mozilla/objdir/dist/bin/firefox ./jprof-log > tmp.html
</pre>
This will generate the file <code>tmp.html</code> which you should view in a
web browser.
<pre>
./jprof --output-dir=/tmp /home/user/mozilla/objdir/dist/bin/firefox ./jprof-log*
</pre>
This will generate a set of files in /tmp for each process.
<h3><a name="interpretation">Interpretation</a></h3>
The Jprof output is split into a flat portion and a hierarchical portion.
There are links to each section at the top of the page. It is typically
easier to analyze the profile by starting with the flat output and following
the links contained in the flat output up to the hierarchical output.
<h4><a name="flat">Flat output</a></h3>
The flat portion of the profile indicates which functions were executing
when the timer was going off. It is displayed as a list of functions names
on the right and the number of times that function was interrupted on the
left. The list is sorted by decreasing interrupt count. For example:
<blockquote> <pre>
Total hit count: 151603
Count %Total Function Name
<a href="#23081">8806 5.8 __libc_poll</a>
<a href="#40008">2254 1.5 __i686.get_pc_thunk.bx</a>
<a href="#21390">2053 1.4 _int_malloc</a>
<a href="#49013">1777 1.2 nsStyleContext::GetStyleData(nsStyleStructID)</a>
<a href="#21380">1600 1.1 __libc_malloc</a>
<a href="#603">1552 1.0 nsCOMPtr_base::~nsCOMPtr_base()</a>
</pre> </blockquote>
This shows that of the 151603 times the timer fired, 1777 (1.2% of the total) were inside nsStyleContext::GetStyleData() and 1552 (1.0% of the total) were in the nsCOMPtr_base destructor.
<p>
In general, the functions with the highest count are the functions which
are taking the most time.
<P>
The function names are linked to the entry for that function in the
hierarchical profile, which is described in the next section.
<h4><a name="hier">Hierarchical output</a></h4>
The hierarchical output is divided up into sections, with each section
corresponding to one function. A typical section looks something like
this:
<blockquote><pre>
index Count Hits Function Name
<A href="#72871"> 545 (46.4%) nsBlockFrame::ReflowInlineFrames(nsBlockReflowState&, nsLineList_iterator, int*)</A>
<A href="#72873"> 100 (8.5%) nsBlockFrame::ReflowDirtyLines(nsBlockReflowState&)</A>
72870 4 (0.3%) <a name=72870> 645 (54.9%)</a> <b>nsBlockFrame::DoReflowInlineFrames(nsBlockReflowState&, nsLineLayout&, nsLineList_iterator, nsFlowAreaRect&, int&, nsFloatManager::SavedState*, int*, LineReflowStatus*, int)</b>
<A href="#72821"> 545 (46.4%) nsBlockFrame::ReflowInlineFrame(nsBlockReflowState&, nsLineLayout&, nsLineList_iterator, nsIFrame*, LineReflowStatus*)</A>
<A href="#72853"> 83 (7.1%) nsBlockFrame::PlaceLine(nsBlockReflowState&, nsLineLayout&, nsLineList_iterator, nsFloatManager::SavedState*, nsRect&, int&, int*)</A>
<A href="#74150"> 9 (0.8%) nsLineLayout::BeginLineReflow(int, int, int, int, int, int)</A>
<A href="#74897"> 1 (0.1%) nsTextFrame::GetType() const</A>
<A href="#74131"> 1 (0.1%) nsLineLayout::RelativePositionFrames(nsOverflowAreas&)</A>
<A href="#58320"> 1 (0.1%) __i686.get_pc_thunk.bx</A>
<A href="#53077"> 1 (0.1%) PL_ArenaAllocate</A>
</pre></blockquote>
The information this block tells us is:
<ul>
<li>There were 4 profiler hits <em>in</em> <code>nsBlockFrame::DoReflowInlineFrames</code>
<li>There were 645 profiler hits <em>in or under</em> <code>nsBlockFrame::DoReflowInlineFrames</code>. Of these:
<ul>
<li>545 were in or under <code>nsBlockFrame::ReflowInlineFrame</code>
<li>83 were in or under <code>nsBlockFrame::PlaceLine</code>
<li>9 were in or under <code>nsLineLayout::BeginLineReflow</code>
<li>1 was in or under <code>nsTextFrame::GetType</code>
<li>1 was in or under <code>nsLineLayout::RelativePositionFrames</code>
<li>1 was in or under <code>__i686.get_pc_thunk.bx</code>
<li>1 was in or under <code>PL_ArenaAllocate</code>
</ul>
<li>Of these 645 calls into <code>nsBlockFrame::DoReflowInlineFrames</code>:
<ul>
<li>545 came from <code>nsBlockFrame::ReflowInlineFrames</code>
<li>100 came from <code>nsBlockFrame::ReflowDirtyLines</code>
</ul>
</ul>
The rest of this section explains how to read this information off from the jprof output.
<p>This block corresponds to the function <code>nsBlockFrame::DoReflowInlineFrames</code>, which is
therefore bolded and not a link. The name of this function is preceded by
five numbers which have the following meaning. The number on the left (72870)
is the index number, and is not important. The next number (4) and the
percentage following (0.3%) are the number
of times this function was interrupted by the timer and the percentage of
the total hits that is. The last number pair ("645 (54.9%)")
are the number of times this function was in the call stack when the timer went
off. That is, the timer went off while we were in code that was ultimately
called from <code>nsBlockFrame::DoReflowInlineFrames</code>.
<p>For our example we can see that our function was in the call stack for
645 interrupt ticks, but we were only the function that was running when
the interrupt arrived 4 times.
<P>
The functions listed above the line for <code>nsBlockFrame::DoReflowInlineFrames</code> are its
callers. The numbers to the left of these function names are the numbers of
times these functions were in the call stack as callers of
<code>nsBlockFrame::DoReflowInlineFrames</code>. In our example, we were called 545 times by
<code>nsBlockFrame::ReflowInlineFrames</code> and 100 times by
<code>nsBlockFrame::ReflowDirtyLines</code>.
<P>
The functions listed below the line for <code>nsBlockFrame::DoReflowInlineFrames</code> are its
callees. The numbers to the left of the function names are the numbers of
times these functions were in the callstack as callees of
<code>nsBlockFrame::DoReflowInlineFrames</code> and the corresponding percentages. In our example, of the 645 profiler hits under <code>nsBlockFrame::DoReflowInlineFrames</code> 545 were under <code>nsBlockFrame::ReflowInlineFrame</code>, 83 were under <code>nsBlockFrame::PlaceLine</code>, and so forth.<p>
<b>NOTE:</b> If there are loops of execution or recursion, the numbers will
not add up and percentages can exceed 100%. If a function directly calls
itself "(self)" will be appended to the line, but indirect recursion will
not be marked.
<h3>Bugs</h3>
The current build of Jprof has only been tested under Ubuntu 8.04 LTS, but
should work under any fairly modern linux distribution using GCC/GLIBC.
Please update this document with any known compatibilities/incompatibilities.
<p>
If you get an error:<p><code>Inconsistency detected by ld.so: dl-open.c: 260: dl_open_worker: Assertion `_dl_debug_initialize (0, args->nsid)->r_state == RT_CONSISTENT' failed!
</code><p>that means you've hit a timing hole in the version of glibc you're
running. See <a
href="http://sources.redhat.com/bugzilla/show_bug.cgi?id=4578">Redhat bug 4578</a>.
<!-- <h3>Update</h3>
<ul>
</ul>
-->
</body>
</html>

231
tools/jprof/bfd.cpp Normal file
View file

@ -0,0 +1,231 @@
// vim:ts=8:sw=2:et:
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "leaky.h"
#ifdef USE_BFD
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <libgen.h>
#include <bfd.h>
#include <cxxabi.h>
static bfd *try_debug_file(const char *filename, unsigned long crc32)
{
int fd = open(filename, O_RDONLY);
if (fd < 0)
return nullptr;
unsigned char buf[4*1024];
unsigned long crc = 0;
while (1) {
ssize_t count = read(fd, buf, sizeof(buf));
if (count <= 0)
break;
crc = bfd_calc_gnu_debuglink_crc32(crc, buf, count);
}
close(fd);
if (crc != crc32)
return nullptr;
bfd *object = bfd_openr(filename, nullptr);
if (!bfd_check_format(object, bfd_object)) {
bfd_close(object);
return nullptr;
}
return object;
}
static bfd *find_debug_file(bfd *lib, const char *aFileName)
{
// check for a separate debug file with symbols
asection *sect = bfd_get_section_by_name(lib, ".gnu_debuglink");
if (!sect)
return nullptr;
bfd_size_type debuglinkSize = bfd_section_size (objfile->obfd, sect);
char *debuglink = new char[debuglinkSize];
bfd_get_section_contents(lib, sect, debuglink, 0, debuglinkSize);
// crc checksum is aligned to 4 bytes, and after the NUL.
int crc_offset = (int(strlen(debuglink)) & ~3) + 4;
unsigned long crc32 = bfd_get_32(lib, debuglink + crc_offset);
// directory component
char *dirbuf = strdup(aFileName);
const char *dir = dirname(dirbuf);
static const char debug_subdir[] = ".debug";
// This is gdb's default global debugging info directory, but gdb can
// be instructed to use a different directory.
static const char global_debug_dir[] = "/usr/lib/debug";
char *filename =
new char[strlen(global_debug_dir) + strlen(dir) + crc_offset + 3];
// /path/debuglink
sprintf(filename, "%s/%s", dir, debuglink);
bfd *debugFile = try_debug_file(filename, crc32);
if (!debugFile) {
// /path/.debug/debuglink
sprintf(filename, "%s/%s/%s", dir, debug_subdir, debuglink);
debugFile = try_debug_file(filename, crc32);
if (!debugFile) {
// /usr/lib/debug/path/debuglink
sprintf(filename, "%s/%s/%s", global_debug_dir, dir, debuglink);
debugFile = try_debug_file(filename, crc32);
}
}
delete[] filename;
free(dirbuf);
delete[] debuglink;
return debugFile;
}
// Use an indirect array to avoid copying tons of objects
Symbol ** leaky::ExtendSymbols(int num)
{
long n = numExternalSymbols + num;
externalSymbols = (Symbol**)
realloc(externalSymbols,
(size_t) (sizeof(externalSymbols[0]) * n));
Symbol *new_array = new Symbol[n];
for (int i = 0; i < num; i++) {
externalSymbols[i + numExternalSymbols] = &new_array[i];
}
lastSymbol = externalSymbols + n;
Symbol **sp = externalSymbols + numExternalSymbols;
numExternalSymbols = n;
return sp;
}
#define NEXT_SYMBOL do { sp++; \
if (sp >= lastSymbol) { \
sp = ExtendSymbols(16384); \
} \
} while (0)
void leaky::ReadSymbols(const char *aFileName, u_long aBaseAddress)
{
int initialSymbols = usefulSymbols;
if (nullptr == externalSymbols) {
externalSymbols = (Symbol**) calloc(sizeof(Symbol*),10000);
Symbol *new_array = new Symbol[10000];
for (int i = 0; i < 10000; i++) {
externalSymbols[i] = &new_array[i];
}
numExternalSymbols = 10000;
}
Symbol** sp = externalSymbols + usefulSymbols;
lastSymbol = externalSymbols + numExternalSymbols;
// Create a dummy symbol for the library so, if it doesn't have any
// symbols, we show it by library.
(*sp)->Init(aFileName, aBaseAddress);
NEXT_SYMBOL;
bfd_boolean kDynamic = (bfd_boolean) false;
static int firstTime = 1;
if (firstTime) {
firstTime = 0;
bfd_init ();
}
bfd* lib = bfd_openr(aFileName, nullptr);
if (nullptr == lib) {
return;
}
if (!bfd_check_format(lib, bfd_object)) {
bfd_close(lib);
return;
}
bfd *symbolFile = find_debug_file(lib, aFileName);
// read mini symbols
PTR minisyms;
unsigned int size;
long symcount = 0;
if (symbolFile) {
symcount = bfd_read_minisymbols(symbolFile, kDynamic, &minisyms, &size);
if (symcount == 0) {
bfd_close(symbolFile);
} else {
bfd_close(lib);
}
}
if (symcount == 0) {
symcount = bfd_read_minisymbols(lib, kDynamic, &minisyms, &size);
if (symcount == 0) {
// symtab is empty; try dynamic symbols
kDynamic = (bfd_boolean) true;
symcount = bfd_read_minisymbols(lib, kDynamic, &minisyms, &size);
}
symbolFile = lib;
}
asymbol* store;
store = bfd_make_empty_symbol(symbolFile);
// Scan symbols
size_t demangle_buffer_size = 128;
char *demangle_buffer = (char*) malloc(demangle_buffer_size);
bfd_byte* from = (bfd_byte *) minisyms;
bfd_byte* fromend = from + symcount * size;
for (; from < fromend; from += size) {
asymbol *sym;
sym = bfd_minisymbol_to_symbol(symbolFile, kDynamic, (const PTR) from, store);
symbol_info syminfo;
bfd_get_symbol_info (symbolFile, sym, &syminfo);
// if ((syminfo.type == 'T') || (syminfo.type == 't')) {
const char* nm = bfd_asymbol_name(sym);
if (nm && nm[0]) {
char* dnm = nullptr;
if (strncmp("__thunk", nm, 7)) {
dnm =
abi::__cxa_demangle(nm, demangle_buffer, &demangle_buffer_size, 0);
if (dnm) {
demangle_buffer = dnm;
}
}
(*sp)->Init(dnm ? dnm : nm, syminfo.value + aBaseAddress);
NEXT_SYMBOL;
}
// }
}
free(demangle_buffer);
demangle_buffer = nullptr;
bfd_close(symbolFile);
int interesting = sp - externalSymbols;
if (!quiet) {
printf("%s provided %d symbols\n", aFileName,
interesting - initialSymbols);
}
usefulSymbols = interesting;
}
#endif /* USE_BFD */

99
tools/jprof/coff.cpp Normal file
View file

@ -0,0 +1,99 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "leaky.h"
#ifdef USE_COFF
#define LANGUAGE_C
#include <sym.h>
#include <cmplrs/stsupport.h>
#include <symconst.h>
#include <filehdr.h>
#include <ldfcn.h>
#include <string.h>
#include <stdlib.h>
#ifdef IRIX4
extern "C" {
extern char *demangle(char const* in);
};
#else
#include <dem.h>
#endif
static char *Demangle(char *rawName)
{
#ifdef IRIX4
return strdup(demangle(rawName));
#else
char namebuf[4000];
demangle(rawName, namebuf);
return strdup(namebuf);
#endif
}
void leaky::readSymbols(const char *fileName)
{
LDFILE *ldptr;
ldptr = ldopen(fileName, nullptr);
if (!ldptr) {
fprintf(stderr, "%s: unable to open \"%s\"\n", applicationName,
fileName);
exit(-1);
}
if (PSYMTAB(ldptr) == 0) {
fprintf(stderr, "%s: \"%s\": has no symbol table\n", applicationName,
fileName);
exit(-1);
}
long isymMax = SYMHEADER(ldptr).isymMax;
long iextMax = SYMHEADER(ldptr).iextMax;
long iMax = isymMax + iextMax;
long alloced = 10000;
Symbol* syms = (Symbol*) malloc(sizeof(Symbol) * 10000);
Symbol* sp = syms;
Symbol* last = syms + alloced;
SYMR symr;
for (long isym = 0; isym < iMax; isym++) {
if (ldtbread(ldptr, isym, &symr) != SUCCESS) {
fprintf(stderr, "%s: can't read symbol #%d\n", applicationName,
isym);
exit(-1);
}
if (isym < isymMax) {
if ((symr.st == stStaticProc)
|| ((symr.st == stProc) &&
((symr.sc == scText) || (symr.sc == scAbs)))
|| ((symr.st == stBlock) &&
(symr.sc == scText))) {
// Text symbol. Set name field to point to the symbol name
sp->name = Demangle(ldgetname(ldptr, &symr));
sp->address = symr.value;
sp++;
if (sp >= last) {
long n = alloced + 10000;
syms = (Symbol*)
realloc(syms, (size_t) (sizeof(Symbol) * n));
last = syms + n;
sp = syms + alloced;
alloced = n;
}
}
}
}
int interesting = sp - syms;
if (!quiet) {
printf("Total of %d symbols\n", interesting);
}
usefulSymbols = interesting;
externalSymbols = syms;
}
#endif /* USE_COFF */

133
tools/jprof/elf.cpp Normal file
View file

@ -0,0 +1,133 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "leaky.h"
#ifdef USE_ELF
#include "leaky.h"
#include <stdio.h>
#include <malloc.h>
#include <libelf/libelf.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
void leaky::readSymbols(const char *fileName)
{
int fd = ::open(fileName, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "%s: unable to open \"%s\"\n", applicationName,
fileName);
exit(-1);
}
elf_version(EV_CURRENT);
Elf *elf = elf_begin(fd, ELF_C_READ, 0);
if (!elf) {
fprintf(stderr, "%s: \"%s\": has no symbol table\n", applicationName,
fileName);
exit(-1);
}
long alloced = 10000;
Symbol* syms = (Symbol*) malloc(sizeof(Symbol) * 10000);
Symbol* sp = syms;
Symbol* last = syms + alloced;
// Get each of the relevant sections and add them to the list of
// symbols.
Elf32_Ehdr *ehdr = elf32_getehdr(elf);
if (!ehdr) {
fprintf(stderr, "%s: elf library lossage\n", applicationName);
exit(-1);
}
#if 0
Elf32_Half ndx = ehdr->e_shstrndx;
#endif
Elf_Scn *scn = 0;
int strtabndx = -1;
for (int i = 1; (scn = elf_nextscn(elf, scn)) != 0; i++) {
Elf32_Shdr *shdr = elf32_getshdr(scn);
#if 0
char *name = elf_strptr(elf, ndx, (size_t) shdr->sh_name);
printf("Section %s (%d 0x%x)\n", name ? name : "(null)",
shdr->sh_type, shdr->sh_type);
#endif
if (shdr->sh_type == SHT_STRTAB) {
/* We assume here that string tables preceed symbol tables... */
strtabndx = i;
continue;
}
#if 0
if (shdr->sh_type == SHT_DYNAMIC) {
/* Dynamic */
Elf_Data *data = elf_getdata(scn, 0);
if (!data || !data->d_size) {
printf("No data...");
continue;
}
Elf32_Dyn *dyn = (Elf32_Dyn*) data->d_buf;
Elf32_Dyn *lastdyn =
(Elf32_Dyn*) ((char*) data->d_buf + data->d_size);
for (; dyn < lastdyn; dyn++) {
printf("tag=%d value=0x%x\n", dyn->d_tag, dyn->d_un.d_val);
}
} else
#endif
if ((shdr->sh_type == SHT_SYMTAB) ||
(shdr->sh_type == SHT_DYNSYM)) {
/* Symbol table */
Elf_Data *data = elf_getdata(scn, 0);
if (!data || !data->d_size) {
printf("No data...");
continue;
}
/* In theory we now have the symbols... */
Elf32_Sym *esym = (Elf32_Sym*) data->d_buf;
Elf32_Sym *lastsym =
(Elf32_Sym*) ((char*) data->d_buf + data->d_size);
for (; esym < lastsym; esym++) {
#if 0
char *nm = elf_strptr(elf, strtabndx, (size_t)esym->st_name);
printf("%20s 0x%08x %02x %02x\n",
nm, esym->st_value, ELF32_ST_BIND(esym->st_info),
ELF32_ST_TYPE(esym->st_info));
#endif
if ((esym->st_value == 0) ||
(ELF32_ST_BIND(esym->st_info) == STB_WEAK) ||
(ELF32_ST_BIND(esym->st_info) == STB_NUM) ||
(ELF32_ST_TYPE(esym->st_info) != STT_FUNC)) {
continue;
}
#if 1
char *nm = elf_strptr(elf, strtabndx, (size_t)esym->st_name);
#endif
sp->name = nm ? strdup(nm) : "(no name)";
sp->address = esym->st_value;
sp++;
if (sp >= last) {
long n = alloced + 10000;
syms = (Symbol*)
realloc(syms, (size_t) (sizeof(Symbol) * n));
last = syms + n;
sp = syms + alloced;
alloced = n;
}
}
}
}
int interesting = sp - syms;
if (!quiet) {
printf("Total of %d symbols\n", interesting);
}
usefulSymbols = interesting;
externalSymbols = syms;
}
#endif /* USE_ELF */

71
tools/jprof/intcnt.cpp Normal file
View file

@ -0,0 +1,71 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "intcnt.h"
IntCount::IntCount() : numInts(0), iPair(nullptr) { }
IntCount::~IntCount() { delete [] iPair;}
int IntCount::getSize() {return numInts;}
int IntCount::getCount(int pos) {return iPair[pos].cnt;}
int IntCount::getIndex(int pos) {return iPair[pos].idx;}
void IntCount::clear()
{
delete[] iPair;
iPair = new IntPair[0];
numInts = 0;
}
int IntCount::countAdd(int index, int increment)
{
if(numInts) {
// Do a binary search to find the element
int divPoint = 0;
if(index>iPair[numInts-1].idx) {
divPoint = numInts;
} else if(index<iPair[0].idx) {
divPoint = 0;
} else {
int low=0, high=numInts-1;
int mid = (low+high)/2;
while(1) {
mid = (low+high)/2;
if(index<iPair[mid].idx) {
high = mid;
} else if(index>iPair[mid].idx) {
if(mid<numInts-1 && index<iPair[mid+1].idx) {
divPoint = mid+1;
break;
} else {
low = mid+1;
}
} else if(index==iPair[mid].idx) {
return iPair[mid].cnt += increment;
}
}
}
int i;
IntPair *tpair = new IntPair[numInts+1];
for(i=0; i<divPoint; i++) {
tpair[i] = iPair[i];
}
for(i=divPoint; i<numInts; i++) {
tpair[i+1] = iPair[i];
}
++numInts;
delete [] iPair;
iPair = tpair;
iPair[divPoint].idx = index;
iPair[divPoint].cnt = increment;
return increment;
} else {
iPair = new IntPair[1];
numInts = 1;
iPair[0].idx = index;
return iPair[0].cnt = increment;
}
}

38
tools/jprof/intcnt.h Normal file
View file

@ -0,0 +1,38 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef INTCNT_H
#define INTCNT_H
class IntCount
{
public:
IntCount();
~IntCount();
void clear();
int countAdd(int index, int increment=1);
int countGet(int index);
int getSize();
int getCount(int pos);
int getIndex(int pos);
IntCount(const IntCount&old)
{
numInts = old.numInts;
if (numInts > 0) {
iPair = new IntPair[numInts];
for (int i = 0; i < numInts; i++) {
iPair[i] = old.iPair[i];
}
} else {
iPair = nullptr;
}
}
private:
int numInts;
struct IntPair{int idx; int cnt;} *iPair;
};
#endif

46
tools/jprof/jprofsig Normal file
View file

@ -0,0 +1,46 @@
#!/bin/sh
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Find Mozilla PID and send it a signal, to be used
# with the jprof tool.
#
jpsignal_usage() {
echo "Usage: jprofsig [start|stop]"
exit 1
}
if [ $# != 1 ]; then
echo "Wrong number of arguments."
jpsignal_usage
fi
jpsignal_arg="$1"
# Find & print mozilla PID
tmpmoz=`ps aux | grep mozilla-bin | head -1 | awk '{ print $2 }'`
echo "Mozilla PID = $tmpmoz"
# See how we were called.
case "$jpsignal_arg" in
start)
if [ "$JP_REALTIME" = 1 ]; then
kill -ALRM $tmpmoz
else
# Normal, non-realtime mode.
kill -PROF $tmpmoz
fi
;;
stop)
kill -USR1 $tmpmoz
;;
*)
jpsignal_usage
exit 1
esac
exit 0

863
tools/jprof/leaky.cpp Normal file
View file

@ -0,0 +1,863 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "leaky.h"
#include "intcnt.h"
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#ifndef NTO
#include <getopt.h>
#endif
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#ifdef NTO
#include <mem.h>
#endif
#ifndef FALSE
#define FALSE 0
#endif
#ifndef TRUE
#define TRUE 1
#endif
static const u_int DefaultBuckets = 10007; // arbitrary, but prime
static const u_int MaxBuckets = 1000003; // arbitrary, but prime
//----------------------------------------------------------------------
int main(int argc, char** argv)
{
leaky* l = new leaky;
l->initialize(argc, argv);
l->outputfd = stdout;
for (int i = 0; i < l->numLogFiles; i++) {
if (l->output_dir || l->numLogFiles > 1) {
char name[2048]; // XXX fix
if (l->output_dir)
snprintf(name,sizeof(name),"%s/%s.html",l->output_dir,argv[l->logFileIndex + i]);
else
snprintf(name,sizeof(name),"%s.html",argv[l->logFileIndex + i]);
fprintf(stderr,"opening %s\n",name);
l->outputfd = fopen(name,"w");
// if an error we won't process the file
}
if (l->outputfd) { // paranoia
l->open(argv[l->logFileIndex + i]);
if (l->outputfd != stderr) {
fclose(l->outputfd);
l->outputfd = nullptr;
}
}
}
return 0;
}
char *
htmlify(const char *in)
{
const char *p = in;
char *out, *q;
int n = 0;
size_t newlen;
// Count the number of '<' and '>' in the input.
while ((p = strpbrk(p, "<>")))
{
++n;
++p;
}
// Knowing the number of '<' and '>', we can calculate the space
// needed for the output string.
newlen = strlen(in) + n * 3 + 1;
out = new char[newlen];
// Copy the input to the output, with substitutions.
p = in;
q = out;
do
{
if (*p == '<')
{
strcpy(q, "&lt;");
q += 4;
}
else if (*p == '>')
{
strcpy(q, "&gt;");
q += 4;
}
else
{
*q++ = *p;
}
p++;
} while (*p);
*q = '\0';
return out;
}
leaky::leaky()
{
applicationName = nullptr;
progFile = nullptr;
quiet = true;
showAddress = false;
showThreads = false;
stackDepth = 100000;
onlyThread = 0;
cleo = false;
mappedLogFile = -1;
firstLogEntry = lastLogEntry = 0;
sfd = -1;
externalSymbols = 0;
usefulSymbols = 0;
numExternalSymbols = 0;
lowestSymbolAddr = 0;
highestSymbolAddr = 0;
loadMap = nullptr;
collect_last = false;
collect_start = -1;
collect_end = -1;
}
leaky::~leaky()
{
}
void leaky::usageError()
{
fprintf(stderr, "Usage: %s [-v] [-t] [-e exclude] [-i include] [-s stackdepth] [--last] [--all] [--start n [--end m]] [--cleo] [--output-dir dir] prog log [log2 ...]\n", (char*) applicationName);
fprintf(stderr,
"\t-v: verbose\n"
"\t-t | --threads: split threads\n"
"\t--only-thread n: only profile thread N\n"
"\t-i include-id: stack must include specified id\n"
"\t-e exclude-id: stack must NOT include specified id\n"
"\t-s stackdepth: Limit depth looked at from captured stack frames\n"
"\t--last: only profile the last capture section\n"
"\t--start n [--end m]: profile n to m (or end) capture sections\n"
"\t--cleo: format output for 'cleopatra' display\n"
"\t--output-dir dir: write output files to dir\n"
"\tIf there's one log, output goes to stdout unless --output-dir is set\n"
"\tIf there are more than one log, output files will be named with .html added\n"
);
exit(-1);
}
static struct option longopts[] = {
{ "threads", 0, nullptr, 't' },
{ "only-thread", 1, nullptr, 'T' },
{ "last", 0, nullptr, 'l' },
{ "start", 1, nullptr, 'x' },
{ "end", 1, nullptr, 'n' },
{ "cleo",0, nullptr, 'c' },
{ "output-dir", 1, nullptr, 'd' },
{ nullptr, 0, nullptr, 0 },
};
void leaky::initialize(int argc, char** argv)
{
applicationName = argv[0];
applicationName = strrchr(applicationName, '/');
if (!applicationName) {
applicationName = argv[0];
} else {
applicationName++;
}
int arg;
int errflg = 0;
int longindex = 0;
onlyThread = 0;
output_dir = nullptr;
cleo = false;
// XXX tons of cruft here left over from tracemalloc
// XXX The -- options shouldn't need short versions, or they should be documented
while (((arg = getopt_long(argc, argv, "adEe:gh:i:r:Rs:tT:qvx:ln:",longopts,&longindex)) != -1)) {
switch (arg) {
case '?':
default:
fprintf(stderr,"error: unknown option %c\n",optopt);
errflg++;
break;
case 'a':
break;
case 'A': // not implemented
showAddress = true;
break;
case 'c':
cleo = true;
break;
case 'd':
output_dir = optarg; // reference to an argv pointer
break;
case 'R':
break;
case 'e':
exclusions.add(optarg);
break;
case 'g':
break;
case 'r': // not implemented
roots.add(optarg);
if (!includes.IsEmpty()) {
errflg++;
}
break;
case 'i':
includes.add(optarg);
if (!roots.IsEmpty()) {
errflg++;
}
break;
case 'h':
break;
case 's':
stackDepth = atoi(optarg);
if (stackDepth < 2) {
stackDepth = 2;
}
break;
case 'x':
// --start
collect_start = atoi(optarg);
break;
case 'n':
// --end
collect_end = atoi(optarg);
break;
case 'l':
// --last
collect_last = true;
break;
case 'q':
break;
case 'v':
quiet = !quiet;
break;
case 't':
showThreads = true;
break;
case 'T':
showThreads = true;
onlyThread = atoi(optarg);
break;
}
}
if (errflg || ((argc - optind) < 2)) {
usageError();
}
progFile = argv[optind++];
logFileIndex = optind;
numLogFiles = argc - optind;
if (!quiet)
fprintf(stderr,"numlogfiles = %d\n",numLogFiles);
}
static void* mapFile(int fd, u_int flags, off_t* sz)
{
struct stat sb;
if (fstat(fd, &sb) < 0) {
perror("fstat");
exit(-1);
}
void* base = mmap(0, (int)sb.st_size, flags, MAP_PRIVATE, fd, 0);
if (!base) {
perror("mmap");
exit(-1);
}
*sz = sb.st_size;
return base;
}
void leaky::LoadMap()
{
malloc_map_entry mme;
char name[1000];
if (!loadMap) {
// all files use the same map
int fd = ::open(M_MAPFILE, O_RDONLY);
if (fd < 0) {
perror("open: " M_MAPFILE);
exit(-1);
}
for (;;) {
int nb = read(fd, &mme, sizeof(mme));
if (nb != sizeof(mme)) break;
nb = read(fd, name, mme.nameLen);
if (nb != (int)mme.nameLen) break;
name[mme.nameLen] = 0;
if (!quiet) {
fprintf(stderr,"%s @ %lx\n", name, mme.address);
}
LoadMapEntry* lme = new LoadMapEntry;
lme->address = mme.address;
lme->name = strdup(name);
lme->next = loadMap;
loadMap = lme;
}
close(fd);
}
}
void leaky::open(char *logFile)
{
int threadArray[100]; // should auto-expand
int last_thread = -1;
int numThreads = 0;
int section = -1;
bool collecting = false;
LoadMap();
setupSymbols(progFile);
// open up the log file
if (mappedLogFile)
::close(mappedLogFile);
mappedLogFile = ::open(logFile, O_RDONLY);
if (mappedLogFile < 0) {
perror("open");
exit(-1);
}
off_t size;
firstLogEntry = (malloc_log_entry*) mapFile(mappedLogFile, PROT_READ, &size);
lastLogEntry = (malloc_log_entry*)((char*)firstLogEntry + size);
if (!collect_last || collect_start < 0) {
collecting = true;
}
// First, restrict it to the capture sections specified (all, last, start/end)
// This loop walks through all the call stacks we recorded
for (malloc_log_entry* lep=firstLogEntry;
lep < lastLogEntry;
lep = reinterpret_cast<malloc_log_entry*>(&lep->pcs[lep->numpcs])) {
if (lep->flags & JP_FIRST_AFTER_PAUSE) {
section++;
if (collect_last) {
firstLogEntry = lep;
numThreads = 0;
collecting = true;
}
if (collect_start == section) {
collecting = true;
firstLogEntry = lep;
}
if (collect_end == section) {
collecting = false;
lastLogEntry = lep;
}
if (!quiet)
fprintf(stderr,"New section %d: first=%p, last=%p, collecting=%d\n",
section,(void*)firstLogEntry,(void*)lastLogEntry,collecting);
}
// Capture thread info at the same time
// Find all the threads captured
// pthread/linux docs say the signal can be delivered to any thread in
// the process. In practice, it appears in Linux that it's always
// delivered to the thread that called setitimer(), and each thread can
// have a separate itimer. There's a support library for gprof that
// overlays pthread_create() to set timers in any threads you spawn.
if (showThreads && collecting) {
if (lep->thread != last_thread)
{
int i;
for (i=0; i<numThreads; i++)
{
if (lep->thread == threadArray[i])
break;
}
if (i == numThreads &&
i < (int) (sizeof(threadArray)/sizeof(threadArray[0])))
{
threadArray[i] = lep->thread;
numThreads++;
if (!quiet)
fprintf(stderr,"new thread %d\n",lep->thread);
}
}
}
}
if (!quiet)
fprintf(stderr,"Done collecting: sections %d: first=%p, last=%p, numThreads=%d\n",
section,(void*)firstLogEntry,(void*)lastLogEntry,numThreads);
if (!cleo) {
fprintf(outputfd,"<html><head><title>Jprof Profile Report</title></head><body>\n");
fprintf(outputfd,"<h1><center>Jprof Profile Report</center></h1>\n");
}
if (showThreads)
{
fprintf(stderr,"Num threads %d\n",numThreads);
if (!cleo) {
fprintf(outputfd,"<hr>Threads:<p><pre>\n");
for (int i=0; i<numThreads; i++)
{
fprintf(outputfd," <a href=\"#thread_%d\">%d</a> ",
threadArray[i],threadArray[i]);
if ((i+1)%10 == 0)
fprintf(outputfd,"<br>\n");
}
fprintf(outputfd,"</pre>");
}
for (int i=0; i<numThreads; i++)
{
if (!onlyThread || onlyThread == threadArray[i])
analyze(threadArray[i]);
}
}
else
{
analyze(0);
}
if (!cleo)
fprintf(outputfd,"</pre></body></html>\n");
}
//----------------------------------------------------------------------
static int symbolOrder(void const* a, void const* b)
{
Symbol const** ap = (Symbol const **)a;
Symbol const** bp = (Symbol const **)b;
return (*ap)->address == (*bp)->address ? 0 :
((*ap)->address > (*bp)->address ? 1 : -1);
}
void leaky::ReadSharedLibrarySymbols()
{
LoadMapEntry* lme = loadMap;
while (nullptr != lme) {
ReadSymbols(lme->name, lme->address);
lme = lme->next;
}
}
void leaky::setupSymbols(const char *fileName)
{
if (usefulSymbols == 0) {
// only read once!
// Read in symbols from the program
ReadSymbols(fileName, 0);
// Read in symbols from the .so's
ReadSharedLibrarySymbols();
if (!quiet) {
fprintf(stderr,"A total of %d symbols were loaded\n", usefulSymbols);
}
// Now sort them
qsort(externalSymbols, usefulSymbols, sizeof(Symbol *), symbolOrder);
lowestSymbolAddr = externalSymbols[0]->address;
highestSymbolAddr = externalSymbols[usefulSymbols-1]->address;
}
}
// Binary search the table, looking for a symbol that covers this
// address.
int leaky::findSymbolIndex(u_long addr)
{
u_int base = 0;
u_int limit = usefulSymbols - 1;
Symbol** end = &externalSymbols[limit];
while (base <= limit) {
u_int midPoint = (base + limit)>>1;
Symbol** sp = &externalSymbols[midPoint];
if (addr < (*sp)->address) {
if (midPoint == 0) {
return -1;
}
limit = midPoint - 1;
} else {
if (sp+1 < end) {
if (addr < (*(sp+1))->address) {
return midPoint;
}
} else {
return midPoint;
}
base = midPoint + 1;
}
}
return -1;
}
Symbol* leaky::findSymbol(u_long addr)
{
int idx = findSymbolIndex(addr);
if(idx<0) {
return nullptr;
} else {
return externalSymbols[idx];
}
}
//----------------------------------------------------------------------
bool leaky::excluded(malloc_log_entry* lep)
{
if (exclusions.IsEmpty()) {
return false;
}
char** pcp = &lep->pcs[0];
u_int n = lep->numpcs;
for (u_int i = 0; i < n; i++, pcp++) {
Symbol* sp = findSymbol((u_long) *pcp);
if (sp && exclusions.contains(sp->name)) {
return true;
}
}
return false;
}
bool leaky::included(malloc_log_entry* lep)
{
if (includes.IsEmpty()) {
return true;
}
char** pcp = &lep->pcs[0];
u_int n = lep->numpcs;
for (u_int i = 0; i < n; i++, pcp++) {
Symbol* sp = findSymbol((u_long) *pcp);
if (sp && includes.contains(sp->name)) {
return true;
}
}
return false;
}
//----------------------------------------------------------------------
void leaky::displayStackTrace(FILE* out, malloc_log_entry* lep)
{
char** pcp = &lep->pcs[0];
u_int n = (lep->numpcs < stackDepth) ? lep->numpcs : stackDepth;
for (u_int i = 0; i < n; i++, pcp++) {
u_long addr = (u_long) *pcp;
Symbol* sp = findSymbol(addr);
if (sp) {
fputs(sp->name, out);
if (showAddress) {
fprintf(out, "[%p]", (char*)addr);
}
}
else {
fprintf(out, "<%p>", (char*)addr);
}
fputc(' ', out);
}
fputc('\n', out);
}
void leaky::dumpEntryToLog(malloc_log_entry* lep)
{
printf("%ld\t", lep->delTime);
printf(" --> ");
displayStackTrace(outputfd, lep);
}
void leaky::generateReportHTML(FILE *fp, int *countArray, int count, int thread)
{
fprintf(fp,"<center>");
if (showThreads)
{
fprintf(fp,"<hr><A NAME=thread_%d><b>Thread: %d</b></A><p>",
thread,thread);
}
fprintf(fp,"<A href=#flat_%d>flat</A><b> | </b><A href=#hier_%d>hierarchical</A>",
thread,thread);
fprintf(fp,"</center><P><P><P>\n");
int totalTimerHits = count;
int *rankingTable = new int[usefulSymbols];
for(int cnt=usefulSymbols; --cnt>=0; rankingTable[cnt]=cnt);
// Drat. I would use ::qsort() but I would need a global variable and my
// intro-pascal professor threatened to flunk anyone who used globals.
// She damaged me for life :-) (That was 1986. See how much influence
// she had. I don't remember her name but I always feel guilty about globals)
// Shell Sort. 581130733 is the max 31 bit value of h = 3h+1
int mx, i, h;
for(mx=usefulSymbols/9, h=581130733; h>0; h/=3) {
if(h<mx) {
for(i = h-1; i<usefulSymbols; i++) {
int j, tmp=rankingTable[i], val = countArray[tmp];
for(j = i; (j>=h) && (countArray[rankingTable[j-h]]<val); j-=h) {
rankingTable[j] = rankingTable[j-h];
}
rankingTable[j] = tmp;
}
}
}
// Ok, We are sorted now. Let's go through the table until we get to
// functions that were never called. Right now we don't do much inside
// this loop. Later we can get callers and callees into it like gprof
// does
fprintf(fp,
"<h2><A NAME=hier_%d></A><center><a href=\"http://dxr.mozilla.org/mozilla-central/source/tools/jprof/README.html#hier\">Hierarchical Profile</a></center></h2><hr>\n",
thread);
fprintf(fp, "<pre>\n");
fprintf(fp, "%6s %6s %4s %s\n",
"index", "Count", "Hits", "Function Name");
for(i=0; i<usefulSymbols && countArray[rankingTable[i]]>0; i++) {
Symbol **sp=&externalSymbols[rankingTable[i]];
(*sp)->cntP.printReport(fp, this, rankingTable[i], totalTimerHits);
char *symname = htmlify((*sp)->name);
fprintf(fp, "%6d %6d (%3.1f%%)%s <a name=%d>%8d (%3.1f%%)</a>%s <b>%s</b>\n",
rankingTable[i],
(*sp)->timerHit, ((*sp)->timerHit*1000/totalTimerHits)/10.0,
((*sp)->timerHit*1000/totalTimerHits)/10.0 >= 10.0 ? "" : " ",
rankingTable[i], countArray[rankingTable[i]],
(countArray[rankingTable[i]]*1000/totalTimerHits)/10.0,
(countArray[rankingTable[i]]*1000/totalTimerHits)/10.0 >= 10.0 ? "" : " ",
symname);
delete [] symname;
(*sp)->cntC.printReport(fp, this, rankingTable[i], totalTimerHits);
fprintf(fp, "<hr>\n");
}
fprintf(fp,"</pre>\n");
// OK, Now we want to print the flat profile. To do this we resort on
// the hit count.
// Cut-N-Paste Shell sort from above. The Ranking Table has already been
// populated, so we do not have to reinitialize it.
for(mx=usefulSymbols/9, h=581130733; h>0; h/=3) {
if(h<mx) {
for(i = h-1; i<usefulSymbols; i++) {
int j, tmp=rankingTable[i], val = externalSymbols[tmp]->timerHit;
for(j = i;
(j>=h) && (externalSymbols[rankingTable[j-h]]->timerHit<val); j-=h) {
rankingTable[j] = rankingTable[j-h];
}
rankingTable[j] = tmp;
}
}
}
// Pre-count up total counter hits, to get a percentage.
// I wanted the total before walking the list, if this
// double-pass over externalSymbols gets slow we can
// do single-pass and print this out after the loop finishes.
totalTimerHits = 0;
for(i=0;
i<usefulSymbols && externalSymbols[rankingTable[i]]->timerHit>0; i++) {
Symbol **sp=&externalSymbols[rankingTable[i]];
totalTimerHits += (*sp)->timerHit;
}
if (totalTimerHits == 0)
totalTimerHits = 1;
if (totalTimerHits != count)
fprintf(stderr,"Hit count mismatch: count=%d; totalTimerHits=%d",
count,totalTimerHits);
fprintf(fp,"<h2><A NAME=flat_%d></A><center><a href=\"http://dxr.mozilla.org/mozilla-central/source/tools/jprof/README.html#flat\">Flat Profile</a></center></h2><br>\n",
thread);
fprintf(fp, "<pre>\n");
fprintf(fp, "Total hit count: %d\n", totalTimerHits);
fprintf(fp, "Count %%Total Function Name\n");
// Now loop for as long as we have timer hits
for(i=0;
i<usefulSymbols && externalSymbols[rankingTable[i]]->timerHit>0; i++) {
Symbol **sp=&externalSymbols[rankingTable[i]];
char *symname = htmlify((*sp)->name);
fprintf(fp, "<a href=\"#%d\">%3d %-2.1f %s</a>\n",
rankingTable[i], (*sp)->timerHit,
((float)(*sp)->timerHit/(float)totalTimerHits)*100.0, symname);
delete [] symname;
}
}
void leaky::analyze(int thread)
{
int *countArray = new int[usefulSymbols];
int *flagArray = new int[usefulSymbols];
//Zero our function call counter
memset(countArray, 0, sizeof(countArray[0])*usefulSymbols);
// reset hit counts
for(int i=0; i<usefulSymbols; i++) {
externalSymbols[i]->timerHit = 0;
externalSymbols[i]->regClear();
}
// The flag array is used to prevent counting symbols multiple times
// if functions are called recursively. In order to keep from having
// to zero it on each pass through the loop, we mark it with the value
// of stacks on each trip through the loop. This means we can determine
// if we have seen this symbol for this stack trace w/o having to reset
// from the prior stacktrace.
memset(flagArray, -1, sizeof(flagArray[0])*usefulSymbols);
if (cleo)
fprintf(outputfd,"m-Start\n");
// This loop walks through all the call stacks we recorded
// --last, --start and --end can restrict it, as can excludes/includes
stacks = 0;
for(malloc_log_entry* lep=firstLogEntry;
lep < lastLogEntry;
lep = reinterpret_cast<malloc_log_entry*>(&lep->pcs[lep->numpcs])) {
if ((thread != 0 && lep->thread != thread) ||
excluded(lep) || !included(lep))
{
continue;
}
++stacks; // How many stack frames did we collect
u_int n = (lep->numpcs < stackDepth) ? lep->numpcs : stackDepth;
char** pcp = &lep->pcs[n-1];
int idx=-1, parrentIdx=-1; // Init idx incase n==0
if (cleo) {
// This loop walks through every symbol in the call stack. By walking it
// backwards we know who called the function when we get there.
char type = 's';
for (int i=n-1; i>=0; --i, --pcp) {
idx = findSymbolIndex(reinterpret_cast<u_long>(*pcp));
if(idx>=0) {
// Skip over bogus __restore_rt frames that realtime profiling
// can introduce.
if (i > 0 && !strcmp(externalSymbols[idx]->name, "__restore_rt")) {
--pcp;
--i;
idx = findSymbolIndex(reinterpret_cast<u_long>(*pcp));
if (idx < 0) {
continue;
}
}
Symbol **sp=&externalSymbols[idx];
char *symname = htmlify((*sp)->name);
fprintf(outputfd,"%c-%s\n",type,symname);
delete [] symname;
}
// else can't find symbol - ignore
type = 'c';
}
} else {
// This loop walks through every symbol in the call stack. By walking it
// backwards we know who called the function when we get there.
for (int i=n-1; i>=0; --i, --pcp) {
idx = findSymbolIndex(reinterpret_cast<u_long>(*pcp));
if(idx>=0) {
// Skip over bogus __restore_rt frames that realtime profiling
// can introduce.
if (i > 0 && !strcmp(externalSymbols[idx]->name, "__restore_rt")) {
--pcp;
--i;
idx = findSymbolIndex(reinterpret_cast<u_long>(*pcp));
if (idx < 0) {
continue;
}
}
// If we have not seen this symbol before count it and mark it as seen
if(flagArray[idx]!=stacks && ((flagArray[idx]=stacks) || true)) {
++countArray[idx];
}
// We know who we are and we know who our parrent is. Count this
if(parrentIdx>=0) {
externalSymbols[parrentIdx]->regChild(idx);
externalSymbols[idx]->regParrent(parrentIdx);
}
// inside if() so an unknown in the middle of a stack won't break
// the link!
parrentIdx=idx;
}
}
// idx should be the function that we were in when we received the signal.
if(idx>=0) {
++externalSymbols[idx]->timerHit;
}
}
}
if (!cleo)
generateReportHTML(outputfd, countArray, stacks, thread);
}
void FunctionCount::printReport(FILE *fp, leaky *lk, int parent, int total)
{
const char *fmt = " <A href=\"#%d\">%8d (%3.1f%%)%s %s</A>%s\n";
int nmax, tmax=((~0U)>>1);
do {
nmax=0;
for(int j=getSize(); --j>=0;) {
int cnt = getCount(j);
if(cnt==tmax) {
int idx = getIndex(j);
char *symname = htmlify(lk->indexToName(idx));
fprintf(fp, fmt, idx, getCount(j),
getCount(j)*100.0/total,
getCount(j)*100.0/total >= 10.0 ? "" : " ",
symname,
parent == idx ? " (self)" : "");
delete [] symname;
} else if(cnt<tmax && cnt>nmax) {
nmax=cnt;
}
}
} while((tmax=nmax)>0);
}

122
tools/jprof/leaky.h Normal file
View file

@ -0,0 +1,122 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef __leaky_h_
#define __leaky_h_
#include "config.h"
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include "libmalloc.h"
#include "strset.h"
#include "intcnt.h"
typedef unsigned int u_int;
struct Symbol;
struct leaky;
class FunctionCount : public IntCount
{
public:
void printReport(FILE *fp, leaky *lk, int parent, int total);
};
struct Symbol {
char* name;
u_long address;
int timerHit;
FunctionCount cntP, cntC;
int regChild(int id) {return cntC.countAdd(id, 1);}
int regParrent(int id) {return cntP.countAdd(id, 1);}
void regClear() {cntC.clear(); cntP.clear();}
Symbol() : timerHit(0) {}
void Init(const char* aName, u_long aAddress) {
name = aName ? strdup(aName) : (char *)"";
address = aAddress;
}
};
struct LoadMapEntry {
char* name; // name of .so
u_long address; // base address where it was mapped in
LoadMapEntry* next;
};
struct leaky {
leaky();
~leaky();
void initialize(int argc, char** argv);
void open(char *arg);
char* applicationName;
int logFileIndex;
int numLogFiles;
char* progFile;
FILE* outputfd;
bool quiet;
bool showAddress;
bool showThreads;
bool cleo;
u_int stackDepth;
int onlyThread;
char* output_dir;
int mappedLogFile;
malloc_log_entry* firstLogEntry;
malloc_log_entry* lastLogEntry;
int stacks;
int sfd;
Symbol** externalSymbols;
Symbol** lastSymbol;
int usefulSymbols;
int numExternalSymbols;
StrSet exclusions;
u_long lowestSymbolAddr;
u_long highestSymbolAddr;
LoadMapEntry* loadMap;
bool collect_last;
int collect_start;
int collect_end;
StrSet roots;
StrSet includes;
void usageError();
void LoadMap();
void analyze(int thread);
void dumpEntryToLog(malloc_log_entry* lep);
void insertAddress(u_long address, malloc_log_entry* lep);
void removeAddress(u_long address, malloc_log_entry* lep);
void displayStackTrace(FILE* out, malloc_log_entry* lep);
Symbol ** ExtendSymbols(int num);
void ReadSymbols(const char* fileName, u_long aBaseAddress);
void ReadSharedLibrarySymbols();
void setupSymbols(const char* fileName);
Symbol* findSymbol(u_long address);
bool excluded(malloc_log_entry* lep);
bool included(malloc_log_entry* lep);
const char* indexToName(int idx) {return externalSymbols[idx]->name;}
private:
void generateReportHTML(FILE *fp, int *countArray, int count, int thread);
int findSymbolIndex(u_long address);
};
#endif /* __leaky_h_ */

28
tools/jprof/moz.build Normal file
View file

@ -0,0 +1,28 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ['stub']
Program('jprof')
SOURCES += [
'bfd.cpp',
'coff.cpp',
'elf.cpp',
'intcnt.cpp',
'leaky.cpp',
'strset.cpp',
]
LOCAL_INCLUDES += [
'stub',
]
OS_LIBS += [
'dl',
'bfd',
'iberty',
]

View file

@ -0,0 +1,143 @@
#!/usr/bin/python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This program splits up a jprof profile into multiple files based on a
# list of functions in a text file. First, a complete profile is
# generated. Then, for each line in the text file, a profile is
# generated containing only stacks that go through that line, and also
# excluding all stacks in earlier lines in the text file. This means
# that the text file, from start to end, is splitting out pieces of the
# profile in their own file. Finally, a final profile containing the
# remainder is produced.
# The program takes four arguments:
# (1) The path to jprof.
# (2) The path to the text file describing the splits. The output
# will be placed in the same directory as this file.
# (3) The program that was profiled.
# (4) The jprof-log file generated by the profile, to be split up.
# (Really, all arguments from (3) and later are passed through to
# jprof, so additional arguments could be provided if you want to pass
# additional arguments to jprof.)
# In slightly more detail:
#
# This script uses jprof's includes (-i) and excludes (-e) options to
# split profiles into segments. It takes as input a single text file,
# and from that text file creates a series of jprof profiles in the
# directory the text file is in.
#
# The input file format looks like the following:
#
# poll g_main_poll
# GetRuleCascade CSSRuleProcessor::GetRuleCascade(nsPresContext *, nsIAtom *)
# RuleProcessorData RuleProcessorData::RuleProcessorData(nsPresContext *, nsIContent *, nsRuleWalker *, nsCompatibility *)
#
# From this input file, the script will construct a profile called
# jprof-0.html that contains the whole profile, a profile called
# jprof-1-poll.html that includes only stacks with g_main_poll, a
# profile called jprof-2-GetRuleCascade.html that includes only stacks
# that have GetRuleCascade and do not have g_main_poll, a profile called
# jprof-3-RuleProcessorData.html that includes only stacks that have the
# RuleProcessorData constructor and do not have GetRuleCascade or
# g_main_poll, and a profile called jprof-4.html that includes only
# stacks that do not have any of the three functions in them.
#
# This means that all of the segments of the profile, except
# jprof-0.html, are mutually exclusive. Thus clever ordering of the
# functions in the input file can lead to a logical splitting of the
# profile into segments.
import sys
import subprocess
import os.path
if len(sys.argv) < 5:
sys.stderr.write("Expected arguments: <jprof> <split-file> <program> <jprof-log>\n")
sys.exit(1)
jprof = sys.argv[1]
splitfile = sys.argv[2]
passthrough = sys.argv[3:]
for f in [jprof, splitfile]:
if not os.path.isfile(f):
sys.stderr.write("could not find file: {0}\n".format(f))
sys.exit(1)
def read_splits(splitfile):
"""
Read splitfile (each line of which contains a name, a space, and
then a function name to split on), and return a list of pairs
representing exactly that. (Note that the name cannot contain
spaces, but the function name can, and often does.)
"""
def line_to_split(line):
line = line.strip("\r\n")
idx = line.index(" ")
return (line[0:idx], line[idx+1:])
io = open(splitfile, "r")
result = [line_to_split(line) for line in io]
io.close()
return result
splits = read_splits(splitfile)
def generate_profile(options, destfile):
"""
Run jprof to generate one split of the profile.
"""
args = [jprof] + options + passthrough
print "Generating {0}".format(destfile)
destio = open(destfile, "w")
# jprof expects the "jprof-map" file to be in its current working directory
cwd = None
for option in passthrough:
if option.find("jprof-log"):
cwd = os.path.dirname(option)
if cwd is None:
raise StandardError("no jprof-log option given")
process = subprocess.Popen(args, stdout=destio, cwd=cwd)
process.wait()
destio.close()
if process.returncode != 0:
os.remove(destfile)
sys.stderr.write("Error {0} from command:\n {1}\n".format(process.returncode, " ".join(args)))
sys.exit(process.returncode)
def output_filename(number, splitname):
"""
Return the filename (absolute path) we should use to output the
profile segment with the given number and splitname. Splitname
should be None for the complete profile and the remainder.
"""
def pad_count(i):
result = str(i)
# 0-pad to the same length
result = "0" * (len(str(len(splits) + 1)) - len(result)) + result
return result
name = pad_count(number)
if splitname is not None:
name += "-" + splitname
return os.path.join(os.path.dirname(splitfile),
"jprof-{0}.html".format(name))
# generate the complete profile
generate_profile([], output_filename(0, None))
# generate the listed splits
count = 1
excludes = []
for (splitname, splitfunction) in splits:
generate_profile(excludes + ["-i" + splitfunction],
output_filename(count, splitname))
excludes += ["-e" + splitfunction]
count = count + 1
# generate the remainder after the splits
generate_profile(excludes, output_filename(count, None))

40
tools/jprof/strset.cpp Normal file
View file

@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "strset.h"
#include <malloc.h>
#include <string.h>
StrSet::StrSet()
{
strings = 0;
numstrings = 0;
}
void StrSet::add(const char* s)
{
if (strings) {
strings = (char**) realloc(strings, (numstrings + 1) * sizeof(char*));
} else {
strings = (char**) malloc(sizeof(char*));
}
strings[numstrings] = strdup(s);
numstrings++;
}
int StrSet::contains(const char* s)
{
char** sp = strings;
int i = numstrings;
while (--i >= 0) {
char *ss = *sp++;
if (ss[0] == s[0]) {
if (strcmp(ss, s) == 0) {
return 1;
}
}
}
return 0;
}

19
tools/jprof/strset.h Normal file
View file

@ -0,0 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef __strset_h_
#define __strset_h_
struct StrSet {
StrSet();
void add(const char* string);
int contains(const char* string);
bool IsEmpty() const { return 0 == numstrings; }
char** strings;
int numstrings;
};
#endif /* __strset_h_ */

View file

@ -0,0 +1,8 @@
#! gmake
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# override optimization
MOZ_OPTIMIZE_FLAGS = -fno-omit-frame-pointer

18
tools/jprof/stub/config.h Normal file
View file

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef config_h___
#define config_h___
#define MAX_STACK_CRAWL 500
#define M_LOGFILE "jprof-log"
#define M_MAPFILE "jprof-map"
#if defined(linux) || defined(NTO)
#define USE_BFD
#undef NEED_WRAPPERS
#endif /* linux */
#endif /* config_h___ */

17
tools/jprof/stub/jprof.h Normal file
View file

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef jprof_h___
#define jprof_h___
#include "nscore.h"
#ifdef _IMPL_JPPROF_API
#define JPROF_API(type) NS_EXPORT_(type)
#else
#define JPROF_API(type) NS_IMPORT_(type)
#endif
JPROF_API(void) setupProfilingStuff(void);
#endif /* jprof_h___ */

View file

@ -0,0 +1,790 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
// vim:cindent:sw=4:et:ts=8:
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// The linux glibc hides part of sigaction if _POSIX_SOURCE is defined
#if defined(linux)
#undef _POSIX_SOURCE
#undef _SVID_SOURCE
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#endif
#include <errno.h>
#if defined(linux)
#include <linux/rtc.h>
#include <pthread.h>
#endif
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <ucontext.h>
#include <execinfo.h>
#include "libmalloc.h"
#include "jprof.h"
#include <string.h>
#include <errno.h>
#include <dlfcn.h>
// Must define before including jprof.h
void *moz_xmalloc(size_t size)
{
return malloc(size);
}
void moz_xfree(void *mem)
{
free(mem);
}
#ifdef NTO
#include <sys/link.h>
extern r_debug _r_debug;
#else
#include <link.h>
#endif
#define USE_GLIBC_BACKTRACE 1
// To debug, use #define JPROF_STATIC
#define JPROF_STATIC static
static int gLogFD = -1;
static pthread_t main_thread;
static bool gIsSlave = false;
static int gFilenamePID;
static void startSignalCounter(unsigned long millisec);
static int enableRTCSignals(bool enable);
//----------------------------------------------------------------------
// replace use of atexit()
static void DumpAddressMap();
struct JprofShutdown {
JprofShutdown() {}
~JprofShutdown() {
DumpAddressMap();
}
};
static void RegisterJprofShutdown() {
// This instanciates the dummy class above, and will trigger the class
// destructor when libxul is unloaded. This is equivalent to atexit(),
// but gracefully handles dlclose().
static JprofShutdown t;
}
#if defined(i386) || defined(_i386) || defined(__x86_64__)
JPROF_STATIC void CrawlStack(malloc_log_entry* me,
void* stack_top, void* top_instr_ptr)
{
#if USE_GLIBC_BACKTRACE
// This probably works on more than x86! But we need a way to get the
// top instruction pointer, which is kindof arch-specific
void *array[500];
int cnt, i;
u_long numpcs = 0;
// This is from glibc. A more generic version might use
// libunwind and/or CaptureStackBackTrace() on Windows
cnt = backtrace(&array[0],sizeof(array)/sizeof(array[0]));
// StackHook->JprofLog->CrawlStack
// Then we have sigaction, which replaced top_instr_ptr
array[3] = top_instr_ptr;
for (i = 3; i < cnt; i++)
{
me->pcs[numpcs++] = (char *) array[i];
}
me->numpcs = numpcs;
#else
// original code - this breaks on many platforms
void **bp;
#if defined(__i386)
__asm__( "movl %%ebp, %0" : "=g"(bp));
#elif defined(__x86_64__)
__asm__( "movq %%rbp, %0" : "=g"(bp));
#else
// It would be nice if this worked uniformly, but at least on i386 and
// x86_64, it stopped working with gcc 4.1, because it points to the
// end of the saved registers instead of the start.
bp = __builtin_frame_address(0);
#endif
u_long numpcs = 0;
bool tracing = false;
me->pcs[numpcs++] = (char*) top_instr_ptr;
while (numpcs < MAX_STACK_CRAWL) {
void** nextbp = (void**) *bp++;
void* pc = *bp;
if (nextbp < bp) {
break;
}
if (tracing) {
// Skip the signal handling.
me->pcs[numpcs++] = (char*) pc;
}
else if (pc == top_instr_ptr) {
tracing = true;
}
bp = nextbp;
}
me->numpcs = numpcs;
#endif
}
#endif
//----------------------------------------------------------------------
static int rtcHz;
static int rtcFD = -1;
static bool circular = false;
#if defined(linux) || defined(NTO)
static void DumpAddressMap()
{
// Turn off the timer so we don't get interrupts during shutdown
#if defined(linux)
if (rtcHz) {
enableRTCSignals(false);
} else
#endif
{
startSignalCounter(0);
}
char filename[2048];
if (gIsSlave)
snprintf(filename, sizeof(filename), "%s-%d", M_MAPFILE, gFilenamePID);
else
snprintf(filename, sizeof(filename), "%s", M_MAPFILE);
int mfd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
if (mfd >= 0) {
malloc_map_entry mme;
link_map* map = _r_debug.r_map;
while (nullptr != map) {
if (map->l_name && *map->l_name) {
mme.nameLen = strlen(map->l_name);
mme.address = map->l_addr;
write(mfd, &mme, sizeof(mme));
write(mfd, map->l_name, mme.nameLen);
#if 0
write(1, map->l_name, mme.nameLen);
write(1, "\n", 1);
#endif
}
map = map->l_next;
}
close(mfd);
}
}
#endif
static bool was_paused = true;
JPROF_STATIC void JprofBufferDump();
JPROF_STATIC void JprofBufferClear();
static void ClearProfilingHook(int signum)
{
if (circular) {
JprofBufferClear();
puts("Jprof: cleared circular buffer.");
}
}
static void EndProfilingHook(int signum)
{
if (circular)
JprofBufferDump();
DumpAddressMap();
was_paused = true;
puts("Jprof: profiling paused.");
}
//----------------------------------------------------------------------
// proper usage would be a template, including the function to find the
// size of an entry, or include a size header explicitly to each entry.
#if defined(linux)
#define DUMB_LOCK() pthread_mutex_lock(&mutex);
#define DUMB_UNLOCK() pthread_mutex_unlock(&mutex);
#else
#define DUMB_LOCK() FIXME()
#define DUMB_UNLOCK() FIXME()
#endif
class DumbCircularBuffer
{
public:
DumbCircularBuffer(size_t init_buffer_size) {
used = 0;
buffer_size = init_buffer_size;
buffer = (unsigned char *) malloc(buffer_size);
head = tail = buffer;
#if defined(linux)
pthread_mutexattr_t mAttr;
pthread_mutexattr_settype(&mAttr, PTHREAD_MUTEX_RECURSIVE_NP);
pthread_mutex_init(&mutex, &mAttr);
pthread_mutexattr_destroy(&mAttr);
#endif
}
~DumbCircularBuffer() {
free(buffer);
#if defined(linux)
pthread_mutex_destroy (&mutex);
#endif
}
void clear() {
DUMB_LOCK();
head = tail;
used = 0;
DUMB_UNLOCK();
}
bool empty() {
return head == tail;
}
size_t space_available() {
size_t result;
DUMB_LOCK();
if (tail > head)
result = buffer_size - (tail-head) - 1;
else
result = head-tail - 1;
DUMB_UNLOCK();
return result;
}
void drop(size_t size) {
// assumes correctness!
DUMB_LOCK();
head += size;
if (head >= &buffer[buffer_size])
head -= buffer_size;
used--;
DUMB_UNLOCK();
}
bool insert(void *data, size_t size) {
// can fail if not enough space in the entire buffer
DUMB_LOCK();
if (space_available() < size)
return false;
size_t max_without_wrap = &buffer[buffer_size] - tail;
size_t initial = size > max_without_wrap ? max_without_wrap : size;
#if DEBUG_CIRCULAR
fprintf(stderr,"insert(%d): max_without_wrap %d, size %d, initial %d\n",used,max_without_wrap,size,initial);
#endif
memcpy(tail,data,initial);
tail += initial;
data = ((char *)data)+initial;
size -= initial;
if (size != 0) {
#if DEBUG_CIRCULAR
fprintf(stderr,"wrapping by %d bytes\n",size);
#endif
memcpy(buffer,data,size);
tail = &(((unsigned char *)buffer)[size]);
}
used++;
DUMB_UNLOCK();
return true;
}
// for external access to the buffer (saving)
void lock() {
DUMB_LOCK();
}
void unlock() {
DUMB_UNLOCK();
}
// XXX These really shouldn't be public...
unsigned char *head;
unsigned char *tail;
unsigned int used;
unsigned char *buffer;
size_t buffer_size;
private:
pthread_mutex_t mutex;
};
class DumbCircularBuffer *JprofBuffer;
JPROF_STATIC void
JprofBufferInit(size_t size)
{
JprofBuffer = new DumbCircularBuffer(size);
}
JPROF_STATIC void
JprofBufferClear()
{
fprintf(stderr,"Told to clear JPROF circular buffer\n");
JprofBuffer->clear();
}
JPROF_STATIC size_t
JprofEntrySizeof(malloc_log_entry *me)
{
return offsetof(malloc_log_entry, pcs) + me->numpcs*sizeof(char*);
}
JPROF_STATIC void
JprofBufferAppend(malloc_log_entry *me)
{
size_t size = JprofEntrySizeof(me);
do {
while (JprofBuffer->space_available() < size &&
JprofBuffer->used > 0) {
#if DEBUG_CIRCULAR
fprintf(stderr,"dropping entry: %d in use, %d free, need %d, size_to_free = %d\n",
JprofBuffer->used,JprofBuffer->space_available(),size,JprofEntrySizeof((malloc_log_entry *) JprofBuffer->head));
#endif
JprofBuffer->drop(JprofEntrySizeof((malloc_log_entry *) JprofBuffer->head));
}
if (JprofBuffer->space_available() < size)
return;
} while (!JprofBuffer->insert(me,size));
}
JPROF_STATIC void
JprofBufferDump()
{
JprofBuffer->lock();
#if DEBUG_CIRCULAR
fprintf(stderr,"dumping JP_CIRCULAR buffer, %d of %d bytes\n",
JprofBuffer->tail > JprofBuffer->head ?
JprofBuffer->tail - JprofBuffer->head :
JprofBuffer->buffer_size + JprofBuffer->tail - JprofBuffer->head,
JprofBuffer->buffer_size);
#endif
if (JprofBuffer->tail >= JprofBuffer->head) {
write(gLogFD, JprofBuffer->head, JprofBuffer->tail - JprofBuffer->head);
} else {
write(gLogFD, JprofBuffer->head, &(JprofBuffer->buffer[JprofBuffer->buffer_size]) - JprofBuffer->head);
write(gLogFD, JprofBuffer->buffer, JprofBuffer->tail - JprofBuffer->buffer);
}
JprofBuffer->clear();
JprofBuffer->unlock();
}
//----------------------------------------------------------------------
JPROF_STATIC void
JprofLog(u_long aTime, void* stack_top, void* top_instr_ptr)
{
// Static is simply to make debugging tolerable
static malloc_log_entry me;
me.delTime = aTime;
me.thread = syscall(SYS_gettid); //gettid();
if (was_paused) {
me.flags = JP_FIRST_AFTER_PAUSE;
was_paused = 0;
} else {
me.flags = 0;
}
CrawlStack(&me, stack_top, top_instr_ptr);
#ifndef NTO
if (circular) {
JprofBufferAppend(&me);
} else {
write(gLogFD, &me, JprofEntrySizeof(&me));
}
#else
printf("Neutrino is missing the pcs member of malloc_log_entry!! \n");
#endif
}
static int realTime;
/* Lets interrupt at 10 Hz. This is so my log files don't get too large.
* This can be changed to a faster value latter. This timer is not
* programmed to reset, even though it is capable of doing so. This is
* to keep from getting interrupts from inside of the handler.
*/
static void startSignalCounter(unsigned long millisec)
{
struct itimerval tvalue;
tvalue.it_interval.tv_sec = 0;
tvalue.it_interval.tv_usec = 0;
tvalue.it_value.tv_sec = millisec/1000;
tvalue.it_value.tv_usec = (millisec%1000)*1000;
if (realTime) {
setitimer(ITIMER_REAL, &tvalue, nullptr);
} else {
setitimer(ITIMER_PROF, &tvalue, nullptr);
}
}
static long timerMilliSec = 50;
#if defined(linux)
static int setupRTCSignals(int hz, struct sigaction *sap)
{
/* global */ rtcFD = open("/dev/rtc", O_RDONLY);
if (rtcFD < 0) {
perror("JPROF_RTC setup: open(\"/dev/rtc\", O_RDONLY)");
return 0;
}
if (sigaction(SIGIO, sap, nullptr) == -1) {
perror("JPROF_RTC setup: sigaction(SIGIO)");
return 0;
}
if (ioctl(rtcFD, RTC_IRQP_SET, hz) == -1) {
perror("JPROF_RTC setup: ioctl(/dev/rtc, RTC_IRQP_SET, $JPROF_RTC_HZ)");
return 0;
}
if (ioctl(rtcFD, RTC_PIE_ON, 0) == -1) {
perror("JPROF_RTC setup: ioctl(/dev/rtc, RTC_PIE_ON)");
return 0;
}
if (fcntl(rtcFD, F_SETSIG, 0) == -1) {
perror("JPROF_RTC setup: fcntl(/dev/rtc, F_SETSIG, 0)");
return 0;
}
if (fcntl(rtcFD, F_SETOWN, getpid()) == -1) {
perror("JPROF_RTC setup: fcntl(/dev/rtc, F_SETOWN, getpid())");
return 0;
}
return 1;
}
static int enableRTCSignals(bool enable)
{
static bool enabled = false;
if (enabled == enable) {
return 0;
}
enabled = enable;
int flags = fcntl(rtcFD, F_GETFL);
if (flags < 0) {
perror("JPROF_RTC setup: fcntl(/dev/rtc, F_GETFL)");
return 0;
}
if (enable) {
flags |= FASYNC;
} else {
flags &= ~FASYNC;
}
if (fcntl(rtcFD, F_SETFL, flags) == -1) {
if (enable) {
perror("JPROF_RTC setup: fcntl(/dev/rtc, F_SETFL, flags | FASYNC)");
} else {
perror("JPROF_RTC setup: fcntl(/dev/rtc, F_SETFL, flags & ~FASYNC)");
}
return 0;
}
return 1;
}
#endif
JPROF_STATIC void StackHook(
int signum,
siginfo_t *info,
void *ucontext)
{
static struct timeval tFirst;
static int first=1;
size_t millisec = 0;
#if defined(linux)
if (rtcHz && pthread_self() != main_thread) {
// Only collect stack data on the main thread, for now.
return;
}
#endif
if(first && !(first=0)) {
puts("Jprof: received first signal");
#if defined(linux)
if (rtcHz) {
enableRTCSignals(true);
} else
#endif
{
gettimeofday(&tFirst, 0);
millisec = 0;
}
} else {
#if defined(linux)
if (rtcHz) {
enableRTCSignals(true);
} else
#endif
{
struct timeval tNow;
gettimeofday(&tNow, 0);
double usec = 1e6*(tNow.tv_sec - tFirst.tv_sec);
usec += (tNow.tv_usec - tFirst.tv_usec);
millisec = static_cast<size_t>(usec*1e-3);
}
}
gregset_t &gregs = ((ucontext_t*)ucontext)->uc_mcontext.gregs;
#ifdef __x86_64__
JprofLog(millisec, (void*)gregs[REG_RSP], (void*)gregs[REG_RIP]);
#else
JprofLog(millisec, (void*)gregs[REG_ESP], (void*)gregs[REG_EIP]);
#endif
if (!rtcHz)
startSignalCounter(timerMilliSec);
}
NS_EXPORT_(void) setupProfilingStuff(void)
{
static int gFirstTime = 1;
char filename[2048]; // XXX fix
if(gFirstTime && !(gFirstTime=0)) {
int startTimer = 1;
int doNotStart = 1;
int firstDelay = 0;
int append = O_TRUNC;
char *tst = getenv("JPROF_FLAGS");
/* Options from JPROF_FLAGS environment variable:
* JP_DEFER -> Wait for a SIGPROF (or SIGALRM, if JP_REALTIME
* is set) from userland before starting
* to generate them internally
* JP_START -> Install the signal handler
* JP_PERIOD -> Time between profiler ticks
* JP_FIRST -> Extra delay before starting
* JP_REALTIME -> Take stack traces in intervals of real time
* rather than time used by the process (and the
* system for the process). This is useful for
* finding time spent by the X server.
* JP_APPEND -> Append to jprof-log rather than overwriting it.
* This is somewhat risky since it depends on the
* address map staying constant across multiple runs.
* JP_FILENAME -> base filename to use when saving logs. Note that
* this does not affect the mapfile.
* JP_CIRCULAR -> use a circular buffer of size N, write/clear on SIGUSR1
*
* JPROF_SLAVE is set if this is not the first process.
*/
circular = false;
if(tst) {
if(strstr(tst, "JP_DEFER"))
{
doNotStart = 0;
startTimer = 0;
}
if(strstr(tst, "JP_START")) doNotStart = 0;
if(strstr(tst, "JP_REALTIME")) realTime = 1;
if(strstr(tst, "JP_APPEND")) append = O_APPEND;
char *delay = strstr(tst,"JP_PERIOD=");
if(delay) {
double tmp = strtod(delay+strlen("JP_PERIOD="), nullptr);
if (tmp>=1e-3) {
timerMilliSec = static_cast<unsigned long>(1000 * tmp);
} else {
fprintf(stderr,
"JP_PERIOD of %g less than 0.001 (1ms), using 1ms\n",
tmp);
timerMilliSec = 1;
}
}
char *circular_op = strstr(tst,"JP_CIRCULAR=");
if(circular_op) {
size_t size = atol(circular_op+strlen("JP_CIRCULAR="));
if (size < 1000) {
fprintf(stderr,
"JP_CIRCULAR of %lu less than 1000, using 10000\n",
(unsigned long) size);
size = 10000;
}
JprofBufferInit(size);
fprintf(stderr,"JP_CIRCULAR buffer of %lu bytes\n", (unsigned long) size);
circular = true;
}
char *first = strstr(tst, "JP_FIRST=");
if(first) {
firstDelay = atol(first+strlen("JP_FIRST="));
}
char *rtc = strstr(tst, "JP_RTC_HZ=");
if (rtc) {
#if defined(linux)
rtcHz = atol(rtc+strlen("JP_RTC_HZ="));
timerMilliSec = 0; /* This makes JP_FIRST work right. */
realTime = 1; /* It's the _R_TC and all. ;) */
#define IS_POWER_OF_TWO(x) (((x) & ((x) - 1)) == 0)
if (!IS_POWER_OF_TWO(rtcHz) || rtcHz < 2) {
fprintf(stderr, "JP_RTC_HZ must be power of two and >= 2, "
"but %d was provided; using default of 2048\n",
rtcHz);
rtcHz = 2048;
}
#else
fputs("JP_RTC_HZ found, but RTC profiling only supported on "
"Linux!\n", stderr);
#endif
}
const char *f = strstr(tst,"JP_FILENAME=");
if (f)
f = f + strlen("JP_FILENAME=");
else
f = M_LOGFILE;
char *is_slave = getenv("JPROF_SLAVE");
if (!is_slave)
setenv("JPROF_SLAVE","", 0);
gIsSlave = !!is_slave;
gFilenamePID = syscall(SYS_gettid); //gettid();
if (is_slave)
snprintf(filename,sizeof(filename),"%s-%d",f,gFilenamePID);
else
snprintf(filename,sizeof(filename),"%s",f);
// XXX FIX! inherit current capture state!
}
if(!doNotStart) {
if(gLogFD<0) {
gLogFD = open(filename, O_CREAT | O_WRONLY | append, 0666);
if(gLogFD<0) {
fprintf(stderr, "Unable to create " M_LOGFILE);
perror(":");
} else {
struct sigaction action;
sigset_t mset;
// Dump out the address map when we terminate
RegisterJprofShutdown();
main_thread = pthread_self();
//fprintf(stderr,"jprof: main_thread = %u\n",
// (unsigned int)main_thread);
// FIX! probably should block these against each other
// Very unlikely.
sigemptyset(&mset);
action.sa_handler = nullptr;
action.sa_sigaction = StackHook;
action.sa_mask = mset;
action.sa_flags = SA_RESTART | SA_SIGINFO;
#if defined(linux)
if (rtcHz) {
if (!setupRTCSignals(rtcHz, &action)) {
fputs("jprof: Error initializing RTC, NOT "
"profiling\n", stderr);
return;
}
}
if (!rtcHz || firstDelay != 0)
#endif
{
if (realTime) {
sigaction(SIGALRM, &action, nullptr);
}
}
// enable PROF in all cases to simplify JP_DEFER/pause/restart
sigaction(SIGPROF, &action, nullptr);
// make it so a SIGUSR1 will stop the profiling
// Note: It currently does not close the logfile.
// This could be configurable (so that it could
// later be reopened).
struct sigaction stop_action;
stop_action.sa_handler = EndProfilingHook;
stop_action.sa_mask = mset;
stop_action.sa_flags = SA_RESTART;
sigaction(SIGUSR1, &stop_action, nullptr);
// make it so a SIGUSR2 will clear the circular buffer
stop_action.sa_handler = ClearProfilingHook;
stop_action.sa_mask = mset;
stop_action.sa_flags = SA_RESTART;
sigaction(SIGUSR2, &stop_action, nullptr);
printf("Jprof: Initialized signal handler and set "
"timer for %lu %s, %d s "
"initial delay\n",
rtcHz ? rtcHz : timerMilliSec,
rtcHz ? "Hz" : "ms",
firstDelay);
if(startTimer) {
#if defined(linux)
/* If we have an initial delay we can just use
startSignalCounter to set up a timer to fire the
first stackHook after that delay. When that happens
we'll go and switch to RTC profiling. */
if (rtcHz && firstDelay == 0) {
puts("Jprof: enabled RTC signals");
enableRTCSignals(true);
} else
#endif
{
puts("Jprof: started timer");
startSignalCounter(firstDelay*1000 + timerMilliSec);
}
}
}
}
}
} else {
printf("setupProfilingStuff() called multiple times\n");
}
}

View file

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef libmalloc_h___
#define libmalloc_h___
#include <sys/types.h>
#include <malloc.h>
#ifdef __cplusplus
extern "C" {
#endif
#include "config.h"
typedef unsigned long u_long;
// For me->flags
#define JP_FIRST_AFTER_PAUSE 1
// Format of a jprof log entry. This is what's written out to the
// "jprof-log" file.
// It's called malloc_log_entry because the history of jprof is that
// it's a modified version of tracemalloc.
struct malloc_log_entry {
u_long delTime;
u_long numpcs;
unsigned int flags;
int thread;
char* pcs[MAX_STACK_CRAWL];
};
// Format of a malloc map entry; after this struct is nameLen+1 bytes of
// name data.
struct malloc_map_entry {
u_long nameLen;
u_long address; // base address
};
#ifdef __cplusplus
} /* end of extern "C" */
#endif
#endif /* libmalloc_h___ */

View file

@ -0,0 +1,17 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXPORTS += [
'jprof.h',
]
SOURCES += [
'libmalloc.cpp',
]
SharedLibrary('jprof')
DEFINES['_IMPL_JPROF_API'] = True

View file

@ -0,0 +1,302 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<!--
vim:sw=4:ts=4:et:
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>Leak Gauge</title>
<style type="text/css">
pre { margin: 0; }
pre.output { border: medium solid; padding: 1em; margin: 1em; }
</style>
<script type="text/javascript">
function runfile(file) {
var result = "Results of processing log " + file.fileName + " :\n";
var fileReader = new FileReader();
fileReader.onload = function(e)
{
runContents(result, e.target.result);
}
fileReader.readAsText(file, "iso-8859-1");
}
function runContents(result, contents) {
// A hash of objects (keyed by the first word of the line in the log)
// that have two public methods, handle_line and dump (to be called using
// call, above), along with any private data they need.
var handlers = {
"DOMWINDOW": {
count: 0,
windows: {},
handle_line: function(line) {
var match = line.match(/^([0-9a-f]*) (\S*)(.*)/);
if (match) {
var addr = match[1];
var verb = match[2];
var rest = match[3];
if (verb == "created") {
var m = rest.match(/ outer=([0-9a-f]*)$/);
if (!m)
throw "outer expected";
this.windows[addr] = { outer: m[1] };
++this.count;
} else if (verb == "destroyed") {
delete this.windows[addr];
} else if (verb == "SetNewDocument") {
var m = rest.match(/^ (.*)$/);
if (!m)
throw "URI expected";
this.windows[addr][m[1]] = true;
}
}
},
dump: function() {
for (var addr in this.windows) {
var winobj = this.windows[addr];
var outer = winobj.outer;
delete winobj.outer;
result += "Leaked " + (outer == "0" ? "outer" : "inner") +
" window " + addr + " " +
(outer == "0" ? "" : "(outer " + outer + ") ") +
"at address " + addr + ".\n";
for (var uri in winobj) {
result += " ... with URI \"" + uri + "\".\n";
}
}
},
summary: function() {
var len = 0;
for (var w in this.windows)
++len;
result += 'Leaked ' + len + ' out of ' +
this.count + " DOM Windows\n";
}
},
"DOCUMENT": {
count: 0,
docs: {},
handle_line: function(line) {
var match = line.match(/^([0-9a-f]*) (\S*)(.*)/);
if (match) {
var addr = match[1];
var verb = match[2];
var rest = match[3];
if (verb == "created") {
this.docs[addr] = {};
++this.count;
} else if (verb == "destroyed") {
delete this.docs[addr];
} else if (verb == "ResetToURI" ||
verb == "StartDocumentLoad") {
var m = rest.match(/^ (.*)$/);
if (!m)
throw "URI expected";
var uri = m[1];
var doc_info = this.docs[addr];
doc_info[uri] = true;
if ("nim" in doc_info) {
doc_info["nim"][uri] = true;
}
}
}
},
dump: function() {
for (var addr in this.docs) {
var doc = this.docs[addr];
result += "Leaked document at address " + addr + ".\n";
for (var uri in doc) {
if (uri != "nim") {
result += " ... with URI \"" + uri + "\".\n";
}
}
}
},
summary: function() {
var len = 0;
for (var w in this.docs)
++len;
result += 'Leaked ' + len + ' out of ' +
this.count + " documents\n";
}
},
"DOCSHELL": {
count: 0,
shells: {},
handle_line: function(line) {
var match = line.match(/^([0-9a-f]*) (\S*)(.*)/);
if (match) {
var addr = match[1];
var verb = match[2];
var rest = match[3];
if (verb == "created") {
this.shells[addr] = {};
++this.count;
} else if (verb == "destroyed") {
delete this.shells[addr];
} else if (verb == "InternalLoad" ||
verb == "SetCurrentURI") {
var m = rest.match(/^ (.*)$/);
if (!m)
throw "URI expected";
this.shells[addr][m[1]] = true;
}
}
},
dump: function() {
for (var addr in this.shells) {
var doc = this.shells[addr];
result += "Leaked docshell at address " + addr + ".\n";
for (var uri in doc) {
result += " ... which loaded URI \"" + uri + "\".\n";
}
}
},
summary: function() {
var len = 0;
for (var w in this.shells)
++len;
result += 'Leaked ' + len + ' out of ' +
this.count + " docshells\n";
}
},
"NODEINFOMANAGER": {
count: 0,
nims: {},
handle_line: function(line) {
var match = line.match(/^([0-9a-f]*) (\S*)(.*)/);
if (match) {
var addr = match[1];
var verb = match[2];
var rest = match[3];
if (verb == "created") {
this.nims[addr] = {};
++this.count;
} else if (verb == "destroyed") {
delete this.nims[addr];
} else if (verb == "Init") {
var m = rest.match(/^ document=(.*)$/);
if (!m)
throw "document pointer expected";
var nim_info = this.nims[addr];
var doc = m[1];
if (doc != "0") {
var doc_info = handlers["DOCUMENT"].docs[doc];
for (var uri in doc_info) {
nim_info[uri] = true;
}
doc_info["nim"] = nim_info;
}
}
}
},
dump: function() {
for (var addr in this.nims) {
var nim = this.nims[addr];
result += "Leaked content nodes associated with node info manager at address " + addr + ".\n";
for (var uri in nim) {
result += " ... with document URI \"" + uri + "\".\n";
}
}
},
summary: function() {
var len = 0;
for (var w in this.nims)
++len;
result += 'Leaked content nodes in ' + len + ' out of ' +
this.count + " documents\n";
}
}
};
var lines = contents.split(/[\r\n]+/);
for (var j in lines) {
var line = lines[j];
// strip off initial "-", thread id, and thread pointer; separate
// first word and rest
var matches = line.match(/^\-?[0-9]*\[[0-9a-f]*\]: (\S*) (.*)$/);
if (matches) {
var handler = matches[1];
var data = matches[2];
if (typeof(handlers[handler]) != "undefined") {
handlers[handler].handle_line(data);
}
}
}
for (var handler in handlers)
handlers[handler].dump();
if (result.length)
result += "\n";
result += "Summary:\n";
for (var handler in handlers)
handlers[handler].summary();
result += "\n";
var out = document.createElement("pre");
out.className = "output";
out.appendChild(document.createTextNode(result));
document.body.appendChild(out);
}
function run() {
var input = document.getElementById("fileinput");
var files = input.files;
for (var i = 0; i < files.length; ++i)
runfile(files[i]);
// So the user can process the same filename again (after
// overwriting the log), clear the value on the form input so we
// will always get an onchange event.
input.value = "";
}
</script>
</head>
<body>
<h1>Leak Gauge</h1>
<pre>$Id: leak-gauge.html,v 1.8 2008/02/08 19:55:34 dbaron%dbaron.org Exp $</pre>
<p>This script is designed to help testers isolate and simplify testcases
for many classes of leaks (those that involve large graphs of core
data structures) in Mozilla-based browsers. It is designed to print
information about what has leaked by processing a log taken while
running the browser. Such a log can be taken over a long session of
normal browsing and then the log can be processed to find sites that
leak. Once a site is known to leak, the logging can then be repeated
to figure out under what conditions the leak occurs.</p>
<p>The way to create this log is to set the environment variables:</p>
<pre> MOZ_LOG=DOMLeak:5,DocumentLeak:5,nsDocShellLeak:5,NodeInfoManagerLeak:5
MOZ_LOG_FILE=nspr.log <i>(or any other filename of your choice)</i></pre>
<p>in your shell and then run the program.</p>
<ul>
<li>In a Windows command prompt, set environment variables with
<pre> set VAR=value</pre></li>
<li> In an sh-based shell such as bash, set environment variables with
<pre> export VAR=value</pre></li>
<li>In a csh-based shell such as tcsh, set environment variables with
<pre> setenv VAR value</pre></li>
</ul>
<p>Once you have this log from a complete run of the browser (you have
to exit; otherwise it will look like everything leaked), you can load
this page (be careful not to overwrite the log when starting the browser
to load this page) and enter the filename of the log:</p>
<p><input type="file" id="fileinput" onchange="run()"></p>
<p>Then you'll see the output below, which will tell you which of
certain core objects leaked and the URLs associated with those
objects.</p>
</body>
</html>

View file

@ -0,0 +1,239 @@
#!/usr/bin/perl -w
# vim:sw=4:ts=4:et:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# $Id: leak-gauge.pl,v 1.8 2008/02/08 19:55:03 dbaron%dbaron.org Exp $
# This script is designed to help testers isolate and simplify testcases
# for many classes of leaks (those that involve large graphs of core
# data structures) in Mozilla-based browsers. It is designed to print
# information about what has leaked by processing a log taken while
# running the browser. Such a log can be taken over a long session of
# normal browsing and then the log can be processed to find sites that
# leak. Once a site is known to leak, the logging can then be repeated
# to figure out under what conditions the leak occurs.
#
# The way to create this log is to set the environment variables:
# MOZ_LOG=DOMLeak:5,DocumentLeak:5,nsDocShellLeak:5,NodeInfoManagerLeak:5
# MOZ_LOG_FILE=nspr.log (or any other filename of your choice)
# in your shell and then run the program.
# * In a Windows command prompt, set environment variables with
# set VAR=value
# * In an sh-based shell such as bash, set environment variables with
# export VAR=value
# * In a csh-based shell such as tcsh, set environment variables with
# setenv VAR value
#
# Then, after you have exited the browser, run this perl script over the
# log. Either of the following commands should work:
# perl ./path/to/leak-gauge.pl nspr.log
# cat nspr.log | perl ./path/to/leak-gauge.pl
# and it will tell you which of certain core objects leaked and the URLs
# associated with those objects.
# Nobody said I'm not allowed to write my own object system in perl. No
# classes here. Just objects and methods.
sub call {
my $func = shift;
my $obj = shift;
my $funcref = ${$obj}{$func};
&$funcref($obj, @_);
}
# A hash of objects (keyed by the first word of the line in the log)
# that have two public methods, handle_line and dump (to be called using
# call, above), along with any private data they need.
my $handlers = {
"DOMWINDOW" => {
count => 0,
windows => {},
handle_line => sub($$) {
my ($self, $line) = @_;
my $windows = ${$self}{windows};
if ($line =~ /^([0-9a-f]*) (\S*)/) {
my ($addr, $verb, $rest) = ($1, $2, $');
if ($verb eq "created") {
$rest =~ / outer=([0-9a-f]*)$/ || die "outer expected";
my $outer = $1;
${$windows}{$addr} = { outer => $1 };
++${$self}{count};
} elsif ($verb eq "destroyed") {
delete ${$windows}{$addr};
} elsif ($verb eq "SetNewDocument") {
$rest =~ /^ (.*)$/ || die "URI expected";
my $uri = ($1);
${${$windows}{$addr}}{$uri} = 1;
}
}
},
dump => sub ($) {
my ($self) = @_;
my $windows = ${$self}{windows};
foreach my $addr (keys(%{$windows})) {
my $winobj = ${$windows}{$addr};
my $outer = delete ${$winobj}{outer};
print "Leaked " . ($outer eq "0" ? "outer" : "inner") .
" window $addr " .
($outer eq "0" ? "" : "(outer $outer) ") .
"at address $addr.\n";
foreach my $uri (keys(%{$winobj})) {
print " ... with URI \"$uri\".\n";
}
}
},
summary => sub($) {
my ($self) = @_;
my $windows = ${$self}{windows};
print 'Leaked ' . keys(%{$windows}) . ' out of ' .
${$self}{count} . " DOM Windows\n";
}
},
"DOCUMENT" => {
count => 0,
docs => {},
handle_line => sub($$) {
my ($self, $line) = @_;
# This doesn't work; I don't have time to figure out why not.
# my $docs = ${$self}{docs};
my $docs = ${$handlers}{"DOCUMENT"}{docs};
if ($line =~ /^([0-9a-f]*) (\S*)/) {
my ($addr, $verb, $rest) = ($1, $2, $');
if ($verb eq "created") {
${$docs}{$addr} = {};
++${$self}{count};
} elsif ($verb eq "destroyed") {
delete ${$docs}{$addr};
} elsif ($verb eq "ResetToURI" ||
$verb eq "StartDocumentLoad") {
$rest =~ /^ (.*)$/ || die "URI expected";
my $uri = $1;
my $doc_info = ${$docs}{$addr};
${$doc_info}{$uri} = 1;
if (exists(${$doc_info}{"nim"})) {
${$doc_info}{"nim"}{$uri} = 1;
}
}
}
},
dump => sub ($) {
my ($self) = @_;
my $docs = ${$self}{docs};
foreach my $addr (keys(%{$docs})) {
print "Leaked document at address $addr.\n";
foreach my $uri (keys(%{${$docs}{$addr}})) {
print " ... with URI \"$uri\".\n" unless $uri eq "nim";
}
}
},
summary => sub($) {
my ($self) = @_;
my $docs = ${$self}{docs};
print 'Leaked ' . keys(%{$docs}) . ' out of ' .
${$self}{count} . " documents\n";
}
},
"DOCSHELL" => {
count => 0,
shells => {},
handle_line => sub($$) {
my ($self, $line) = @_;
my $shells = ${$self}{shells};
if ($line =~ /^([0-9a-f]*) (\S*)/) {
my ($addr, $verb, $rest) = ($1, $2, $');
if ($verb eq "created") {
${$shells}{$addr} = {};
++${$self}{count};
} elsif ($verb eq "destroyed") {
delete ${$shells}{$addr};
} elsif ($verb eq "InternalLoad" ||
$verb eq "SetCurrentURI") {
$rest =~ /^ (.*)$/ || die "URI expected";
my $uri = $1;
${${$shells}{$addr}}{$uri} = 1;
}
}
},
dump => sub ($) {
my ($self) = @_;
my $shells = ${$self}{shells};
foreach my $addr (keys(%{$shells})) {
print "Leaked docshell at address $addr.\n";
foreach my $uri (keys(%{${$shells}{$addr}})) {
print " ... which loaded URI \"$uri\".\n";
}
}
},
summary => sub($) {
my ($self) = @_;
my $shells = ${$self}{shells};
print 'Leaked ' . keys(%{$shells}) . ' out of ' .
${$self}{count} . " docshells\n";
}
},
"NODEINFOMANAGER" => {
count => 0,
nims => {},
handle_line => sub($$) {
my ($self, $line) = @_;
my $nims = ${$self}{nims};
if ($line =~ /^([0-9a-f]*) (\S*)/) {
my ($addr, $verb, $rest) = ($1, $2, $');
if ($verb eq "created") {
${$nims}{$addr} = {};
++${$self}{count};
} elsif ($verb eq "destroyed") {
delete ${$nims}{$addr};
} elsif ($verb eq "Init") {
$rest =~ /^ document=(.*)$/ ||
die "document pointer expected";
my $doc = $1;
if ($doc ne "0") {
my $nim_info = ${$nims}{$addr};
my $doc_info = ${$handlers}{"DOCUMENT"}{docs}{$doc};
foreach my $uri (keys(%{$doc_info})) {
${$nim_info}{$uri} = 1;
}
${$doc_info}{"nim"} = $nim_info;
}
}
}
},
dump => sub ($) {
my ($self) = @_;
my $nims = ${$self}{nims};
foreach my $addr (keys(%{$nims})) {
print "Leaked content nodes associated with node info manager at address $addr.\n";
foreach my $uri (keys(%{${$nims}{$addr}})) {
print " ... with document URI \"$uri\".\n";
}
}
},
summary => sub($) {
my ($self) = @_;
my $nims = ${$self}{nims};
print 'Leaked content nodes within ' . keys(%{$nims}) . ' out of ' .
${$self}{count} . " documents\n";
}
}
};
while (<>) {
# strip off initial "-", thread id, and thread pointer; separate
# first word and rest
if (/^\-?[0-9]*\[[0-9a-f]*\]: (\S*) ([^\n\r]*)[\n\r]*$/) {
my ($handler, $data) = ($1, $2);
if (defined(${$handlers}{$handler})) {
call("handle_line", ${$handlers}{$handler}, $data);
}
}
}
foreach my $key (keys(%{$handlers})) {
call("dump", ${$handlers}{$key});
}
print "Summary:\n";
foreach my $key (keys(%{$handlers})) {
call("summary", ${$handlers}{$key});
}

192
tools/lint/docs/Makefile Normal file
View file

@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/mozlint.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/mozlint.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/mozlint"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/mozlint"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

112
tools/lint/docs/conf.py Normal file
View file

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
#
# mozlint documentation build configuration file, created by
# sphinx-quickstart on Fri Nov 27 17:38:49 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
import os
# -- General configuration ------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'mozlint'
copyright = u'2015, Andrew Halberstadt'
author = u'Andrew Halberstadt'
# The short X.Y version.
version = '0.1.0'
# The full version, including alpha/beta/rc tags.
release = '0.1.0'
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
if os.environ.get('READTHEDOCS', None) != 'True':
try:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
except ImportError:
pass
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Output file base name for HTML help builder.
htmlhelp_basename = 'mozlintdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'mozlint.tex', u'mozlint Documentation',
u'Andrew Halberstadt', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'mozlint', u'mozlint Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'mozlint', u'mozlint Documentation',
author, 'mozlint', 'One line description of project.',
'Miscellaneous'),
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}

153
tools/lint/docs/create.rst Normal file
View file

@ -0,0 +1,153 @@
Adding a New Linter to the Tree
===============================
A linter is a python file with a ``.lint`` extension and a global dict called LINTER. Depending on how
complex it is, there may or may not be any actual python code alongside the LINTER definition.
Here's a trivial example:
no-eval.lint
.. code-block:: python
LINTER = {
'name': 'EvalLinter',
'description': "Ensures the string 'eval' doesn't show up."
'include': "**/*.js",
'type': 'string',
'payload': 'eval',
}
Now ``no-eval.lint`` gets passed into :func:`LintRoller.read`.
Linter Types
------------
There are three types of linters, though more may be added in the future.
1. string - fails if substring is found
2. regex - fails if regex matches
3. external - fails if a python function returns a non-empty result list
4. structured_log - fails if a mozlog logger emits any lint_error or lint_warning log messages
As seen from the example above, string and regex linters are very easy to create, but they
should be avoided if possible. It is much better to use a context aware linter for the language you
are trying to lint. For example, use eslint to lint JavaScript files, use flake8 to lint python
files, etc.
Which brings us to the third and most interesting type of linter,
external. External linters call an arbitrary python function which is
responsible for not only running the linter, but ensuring the results
are structured properly. For example, an external type could shell out
to a 3rd party linter, collect the output and format it into a list of
:class:`ResultContainer` objects. The signature for this python
function is ``lint(files, **kwargs)``, where ``files`` is a list of
files to lint.
Structured log linters are much like external linters, but suitable
for cases where the linter code is using mozlog and emits
``lint_error`` or ``lint_warning`` logging messages when the lint
fails. This is recommended for writing novel gecko-specific lints. In
this case the signature for lint functions is ``lint(files, logger,
**kwargs)``.
LINTER Definition
-----------------
Each ``.lint`` file must have a variable called LINTER which is a dict containing metadata about the
linter. Here are the supported keys:
* name - The name of the linter (required)
* description - A brief description of the linter's purpose (required)
* type - One of 'string', 'regex' or 'external' (required)
* payload - The actual linting logic, depends on the type (required)
* include - A list of glob patterns that must be matched (optional)
* exclude - A list of glob patterns that must not be matched (optional)
* extensions - A list of file extensions to be considered (optional)
* setup - A function that sets up external dependencies (optional)
In addition to the above, some ``.lint`` files correspond to a single lint rule. For these, the
following additional keys may be specified:
* message - A string to print on infraction (optional)
* hint - A string with a clue on how to fix the infraction (optional)
* rule - An id string for the lint rule (optional)
* level - The severity of the infraction, either 'error' or 'warning' (optional)
For structured_log lints the following additional keys apply:
* logger - A StructuredLog object to use for logging. If not supplied
one will be created (optional)
Example
-------
Here is an example of an external linter that shells out to the python flake8 linter:
.. code-block:: python
import json
import os
import subprocess
from collections import defaultdict
from mozlint import result
FLAKE8_NOT_FOUND = """
Could not find flake8! Install flake8 and try again.
""".strip()
def lint(files, **lintargs):
import which
binary = os.environ.get('FLAKE8')
if not binary:
try:
binary = which.which('flake8')
except which.WhichError:
print(FLAKE8_NOT_FOUND)
return 1
# Flake8 allows passing in a custom format string. We use
# this to help mold the default flake8 format into what
# mozlint's ResultContainer object expects.
cmdargs = [
binary,
'--format',
'{"path":"%(path)s","lineno":%(row)s,"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}',
] + files
proc = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, env=os.environ)
output = proc.communicate()[0]
# all passed
if not output:
return []
results = []
for line in output.splitlines():
# res is a dict of the form specified by --format above
res = json.loads(line)
# parse level out of the id string
if 'code' in res and res['code'].startswith('W'):
res['level'] = 'warning'
# result.from_linter is a convenience method that
# creates a ResultContainer using a LINTER definition
# to populate some defaults.
results.append(result.from_linter(LINTER, **res))
return results
LINTER = {
'name': "flake8",
'description': "Python linter",
'include': ['**/*.py'],
'type': 'external',
'payload': lint,
}

37
tools/lint/docs/index.rst Normal file
View file

@ -0,0 +1,37 @@
Linting
=======
Linters are used in mozilla-central to help enforce coding style and avoid bad practices. Due to the
wide variety of languages in use and the varying style preferences per team, this is not an easy
task. In addition, linters should be runnable from editors, from the command line, from review tools
and from continuous integration. It's easy to see how the complexity of running all of these
different kinds of linters in all of these different places could quickly balloon out of control.
``Mozlint`` is a library that accomplishes two goals:
1. It provides a standard method for adding new linters to the tree, which can be as easy as
defining a json object in a ``.lint`` file. This helps keep lint related code localized, and
prevents different teams from coming up with their own unique lint implementations.
2. It provides a streamlined interface for running all linters at once. Instead of running N
different lint commands to test your patch, a single ``mach lint`` command will automatically run
all applicable linters. This means there is a single API surface that other tools can use to
invoke linters.
``Mozlint`` isn't designed to be used directly by end users. Instead, it can be consumed by things
like mach, mozreview and taskcluster.
.. toctree::
:caption: Linting User Guide
:maxdepth: 2
usage
create
linters/eslint
linters/flake8
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View file

@ -0,0 +1,174 @@
=====================
Mozilla ESLint Plugin
=====================
balanced-listeners
------------------
Checks that for every occurence of 'addEventListener' or 'on' there is an
occurence of 'removeEventListener' or 'off' with the same event name.
components-imports
------------------
Checks the filename of imported files e.g. ``Cu.import("some/path/Blah.jsm")``
adds Blah to the global scope.
import-browserjs-globals
------------------------
When included files from the main browser UI scripts will be loaded and any
declared globals will be defined for the current file. This is mostly useful for
browser-chrome mochitests that call browser functions.
import-globals-from
-------------------
Parses a file for globals defined in various unique Mozilla ways.
When a "import-globals-from <path>" comment is found in a file, then all globals
from the file at <path> will be imported in the current scope. This will also
operate recursively.
This is useful for scripts that are loaded as <script> tag in a window and rely
on each other's globals.
If <path> is a relative path, then it must be relative to the file being
checked by the rule.
import-headjs-globals
---------------------
Import globals from head.js and from any files that were imported by
head.js (as far as we can correctly resolve the path).
The following file import patterns are supported:
- ``Services.scriptloader.loadSubScript(path)``
- ``loader.loadSubScript(path)``
- ``loadSubScript(path)``
- ``loadHelperScript(path)``
- ``import-globals-from path``
If path does not exist because it is generated e.g.
``testdir + "/somefile.js"`` we do our best to resolve it.
The following patterns are supported:
- ``Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");``
- ``loader.lazyImporter(this, "name1");``
- ``loader.lazyRequireGetter(this, "name2"``
- ``loader.lazyServiceGetter(this, "name3"``
- ``XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout", ...)``
- ``loader.lazyGetter(this, "toolboxStrings"``
- ``XPCOMUtils.defineLazyGetter(this, "clipboardHelper"``
mark-test-function-used
-----------------------
Simply marks `test` (the test method) or `run_test` as used when in mochitests
or xpcshell tests respectively. This avoids ESLint telling us that the function
is never called.
no-aArgs
--------
Checks that function argument names don't start with lowercase 'a' followed by
a capital letter. This is to prevent the use of Hungarian notation whereby the
first letter is a prefix that indicates the type or intended use of a variable.
no-cpows-in-tests
-----------------
This rule checks if the file is a browser mochitest and, if so, checks for
possible CPOW usage by checking for the following strings:
- "gBrowser.contentWindow"
- "gBrowser.contentDocument"
- "gBrowser.selectedBrowser.contentWindow"
- "browser.contentDocument"
- "window.content"
- "content"
- "content."
Note: These are string matches so we will miss situations where the parent
object is assigned to another variable e.g.::
var b = gBrowser;
b.content // Would not be detected as a CPOW.
no-single-arg-cu-import
-----------------------
Rejects calls to "Cu.import" that do not supply a second argument (meaning they
add the exported properties into global scope).
reject-importGlobalProperties
-----------------------------
Rejects calls to ``Cu.importGlobalProperties``. Use of this function is
undesirable in some parts of the tree.
reject-some-requires
--------------------
This takes an option, a regular expression. Invocations of
``require`` with a string literal argument are matched against this
regexp; and if it matches, the ``require`` use is flagged.
this-top-level-scope
--------------------
Treats top-level assignments like ``this.mumble = value`` as declaring a global.
Note: These are string matches so we will miss situations where the parent
object is assigned to another variable e.g.::
var b = gBrowser;
b.content // Would not be detected as a CPOW.
var-only-at-top-level
---------------------
Marks all var declarations that are not at the top level invalid.
Example
=======
+-------+-----------------------+
| Possible values for all rules |
+-------+-----------------------+
| Value | Meaning |
+-------+-----------------------+
| 0 | Deactivated |
+-------+-----------------------+
| 1 | Warning |
+-------+-----------------------+
| 2 | Error |
+-------+-----------------------+
Example configuration::
"rules": {
"mozilla/balanced-listeners": 2,
"mozilla/components-imports": 1,
"mozilla/import-globals-from": 1,
"mozilla/import-headjs-globals": 1,
"mozilla/mark-test-function-used": 1,
"mozilla/var-only-at-top-level": 1,
"mozilla/no-cpows-in-tests": 1,
}

View file

@ -0,0 +1,45 @@
ESLint
======
`ESLint`_ is a popular linter for JavaScript.
Run Locally
-----------
The mozlint integration of `ESLint`_ can be run using mach:
.. parsed-literal::
$ mach lint --linter eslint <file paths>
Alternatively, omit the ``--linter eslint`` and run all configured linters, which will include
ESLint.
Configuration
-------------
The `ESLint`_ mozilla-central integration uses a blacklist to exclude certain directories from being
linted. This lives in ``topsrcdir/.eslintignore``. If you don't wish your directory to be linted, it
must be added here.
The global configuration file lives in ``topsrcdir/.eslintrc``. This global configuration can be
overridden by including an ``.eslintrc`` in the appropriate subdirectory. For an overview of the
supported configuration, see `ESLint's documentation`_.
ESLint Plugin Mozilla
---------------------
In addition to default ESLint rules, there are several Mozilla-specific rules that are defined in
the :doc:`Mozilla ESLint Plugin <eslint-plugin-mozilla>`.
.. _ESLint: http://eslint.org/
.. _ESLint's documentation: http://eslint.org/docs/user-guide/configuring
.. toctree::
:hidden:
eslint-plugin-mozilla

View file

@ -0,0 +1,50 @@
Flake8
======
`Flake8`_ is a popular lint wrapper for python. Under the hood, it runs three other tools and
combines their results:
* `pep8`_ for checking style
* `pyflakes`_ for checking syntax
* `mccabe`_ for checking complexity
Run Locally
-----------
The mozlint integration of flake8 can be run using mach:
.. parsed-literal::
$ mach lint --linter flake8 <file paths>
Alternatively, omit the ``--linter flake8`` and run all configured linters, which will include
flake8.
Configuration
-------------
Only directories explicitly whitelisted will have flake8 run against them. To enable flake8 linting
in a source directory, it must be added to the ``include`` directive in ```tools/lint/flake8.lint``.
If you wish to exclude a subdirectory of an included one, you can add it to the ``exclude``
directive.
The default configuration file lives in ``topsrcdir/.flake8``. The default configuration can be
overriden for a given subdirectory by creating a new ``.flake8`` file in the subdirectory. Be warned
that ``.flake8`` files cannot inherit from one another, so all configuration you wish to keep must
be re-defined.
.. warning::
Only ``.flake8`` files that live in a directory that is explicitly included in the ``include``
directive will be considered. See `bug 1277851`_ for more details.
For an overview of the supported configuration, see `flake8's documentation`_.
.. _Flake8: https://flake8.readthedocs.io/en/latest/
.. _pep8: http://pep8.readthedocs.io/en/latest/
.. _pyflakes: https://github.com/pyflakes/pyflakes
.. _mccabe: https://github.com/pycqa/mccabe
.. _bug 1277851: https://bugzilla.mozilla.org/show_bug.cgi?id=1277851
.. _flake8's documentation: https://flake8.readthedocs.io/en/latest/config.html

263
tools/lint/docs/make.bat Normal file
View file

@ -0,0 +1,263 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
echo. coverage to run coverage check of the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
REM Check if sphinx-build is available and fallback to Python version if any
%SPHINXBUILD% 2> nul
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
:sphinx_python
set SPHINXBUILD=python -m sphinx.__init__
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
:sphinx_ok
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\mozlint.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\mozlint.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "coverage" (
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
if errorlevel 1 exit /b 1
echo.
echo.Testing of coverage in the sources finished, look at the ^
results in %BUILDDIR%/coverage/python.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end

41
tools/lint/docs/usage.rst Normal file
View file

@ -0,0 +1,41 @@
Running Linters Locally
=======================
You can run all the various linters in the tree using the ``mach lint`` command. Simply pass in the
directory or file you wish to lint (defaults to current working directory):
.. parsed-literal::
./mach lint path/to/files
Multiple paths are allowed:
.. parsed-literal::
./mach lint path/to/foo.js path/to/bar.py path/to/dir
``Mozlint`` will automatically determine which types of files exist, and which linters need to be run
against them. For example, if the directory contains both JavaScript and Python files then mozlint
will automatically run both ESLint and Flake8 against those files respectively.
To restrict which linters are invoked manually, pass in ``-l/--linter``:
.. parsed-literal::
./mach lint -l eslint path/to/files
Finally, ``mozlint`` can lint the files touched by a set of revisions or the working directory using
the ``-r/--rev`` and ``-w/--workdir`` arguments respectively. These work both with mercurial and
git. In the case of ``--rev`` the value is passed directly to the underlying vcs, so normal revision
specifiers will work. For example, say we want to lint all files touched by the last three commits.
In mercurial, this would be:
.. parsed-literal::
./mach lint -r ".~2::."
In git, this would be:
.. parsed-literal::
./mach lint -r "HEAD~2 HEAD"

368
tools/lint/eslint.lint Normal file
View file

@ -0,0 +1,368 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import os
import platform
import re
import signal
import subprocess
import sys
from distutils.version import LooseVersion
import which
from mozprocess import ProcessHandler
from mozlint import result
ESLINT_ERROR_MESSAGE = """
An error occurred running eslint. Please check the following error messages:
{}
""".strip()
ESLINT_NOT_FOUND_MESSAGE = """
Could not find eslint! We looked at the --binary option, at the ESLINT
environment variable, and then at your local node_modules path. Please Install
eslint and needed plugins with:
mach eslint --setup
and try again.
""".strip()
NODE_NOT_FOUND_MESSAGE = """
nodejs v4.2.3 is either not installed or is installed to a non-standard path.
Please install nodejs from https://nodejs.org and try again.
Valid installation paths:
""".strip()
NPM_NOT_FOUND_MESSAGE = """
Node Package Manager (npm) is either not installed or installed to a
non-standard path. Please install npm from https://nodejs.org (it comes as an
option in the node installation) and try again.
Valid installation paths:
""".strip()
VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
CARET_VERSION_RANGE_RE = re.compile(r"^\^((\d+)\.\d+\.\d+)$")
EXTENSIONS = ['.js', '.jsm', '.jsx', '.xml', '.html', '.xhtml']
project_root = None
def eslint_setup():
"""Ensure eslint is optimally configured.
This command will inspect your eslint configuration and
guide you through an interactive wizard helping you configure
eslint for optimal use on Mozilla projects.
"""
orig_cwd = os.getcwd()
sys.path.append(os.path.dirname(__file__))
module_path = get_eslint_module_path()
# npm sometimes fails to respect cwd when it is run using check_call so
# we manually switch folders here instead.
os.chdir(module_path)
npm_path = get_node_or_npm_path("npm")
if not npm_path:
return 1
# Install ESLint and external plugins
cmd = [npm_path, "install"]
print("Installing eslint for mach using \"%s\"..." % (" ".join(cmd)))
if not call_process("eslint", cmd):
return 1
# Install in-tree ESLint plugin
cmd = [npm_path, "install",
os.path.join(module_path, "eslint-plugin-mozilla")]
print("Installing eslint-plugin-mozilla using \"%s\"..." % (" ".join(cmd)))
if not call_process("eslint-plugin-mozilla", cmd):
return 1
eslint_path = os.path.join(module_path, "node_modules", ".bin", "eslint")
print("\nESLint and approved plugins installed successfully!")
print("\nNOTE: Your local eslint binary is at %s\n" % eslint_path)
os.chdir(orig_cwd)
def call_process(name, cmd, cwd=None):
try:
with open(os.devnull, "w") as fnull:
subprocess.check_call(cmd, cwd=cwd, stdout=fnull)
except subprocess.CalledProcessError:
if cwd:
print("\nError installing %s in the %s folder, aborting." % (name, cwd))
else:
print("\nError installing %s, aborting." % name)
return False
return True
def expected_eslint_modules():
# Read the expected version of ESLint and external modules
expected_modules_path = os.path.join(get_eslint_module_path(), "package.json")
with open(expected_modules_path, "r") as f:
expected_modules = json.load(f)["dependencies"]
# Also read the in-tree ESLint plugin version
mozilla_json_path = os.path.join(get_eslint_module_path(),
"eslint-plugin-mozilla", "package.json")
with open(mozilla_json_path, "r") as f:
expected_modules["eslint-plugin-mozilla"] = json.load(f)["version"]
return expected_modules
def eslint_module_has_issues():
has_issues = False
node_modules_path = os.path.join(get_eslint_module_path(), "node_modules")
for name, version_range in expected_eslint_modules().iteritems():
path = os.path.join(node_modules_path, name, "package.json")
if not os.path.exists(path):
print("%s v%s needs to be installed locally." % (name, version_range))
has_issues = True
continue
data = json.load(open(path))
if not version_in_range(data["version"], version_range):
print("%s v%s should be v%s." % (name, data["version"], version_range))
has_issues = True
return has_issues
def version_in_range(version, version_range):
"""
Check if a module version is inside a version range. Only supports explicit versions and
caret ranges for the moment, since that's all we've used so far.
"""
if version == version_range:
return True
version_match = VERSION_RE.match(version)
if not version_match:
raise RuntimeError("mach eslint doesn't understand module version %s" % version)
version = LooseVersion(version)
# Caret ranges as specified by npm allow changes that do not modify the left-most non-zero
# digit in the [major, minor, patch] tuple. The code below assumes the major digit is
# non-zero.
range_match = CARET_VERSION_RANGE_RE.match(version_range)
if range_match:
range_version = range_match.group(1)
range_major = int(range_match.group(2))
range_min = LooseVersion(range_version)
range_max = LooseVersion("%d.0.0" % (range_major + 1))
return range_min <= version < range_max
return False
def get_possible_node_paths_win():
"""
Return possible nodejs paths on Windows.
"""
if platform.system() != "Windows":
return []
return list({
"%s\\nodejs" % os.environ.get("SystemDrive"),
os.path.join(os.environ.get("ProgramFiles"), "nodejs"),
os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"),
os.path.join(os.environ.get("PROGRAMFILES"), "nodejs")
})
def get_node_or_npm_path(filename, minversion=None):
"""
Return the nodejs or npm path.
"""
if platform.system() == "Windows":
for ext in [".cmd", ".exe", ""]:
try:
node_or_npm_path = which.which(filename + ext,
path=get_possible_node_paths_win())
if is_valid(node_or_npm_path, minversion):
return node_or_npm_path
except which.WhichError:
pass
else:
try:
node_or_npm_path = which.which(filename)
if is_valid(node_or_npm_path, minversion):
return node_or_npm_path
except which.WhichError:
pass
if filename == "node":
print(NODE_NOT_FOUND_MESSAGE)
elif filename == "npm":
print(NPM_NOT_FOUND_MESSAGE)
if platform.system() == "Windows":
app_paths = get_possible_node_paths_win()
for p in app_paths:
print(" - %s" % p)
elif platform.system() == "Darwin":
print(" - /usr/local/bin/node")
elif platform.system() == "Linux":
print(" - /usr/bin/nodejs")
return None
def is_valid(path, minversion=None):
try:
version_str = subprocess.check_output([path, "--version"],
stderr=subprocess.STDOUT)
if minversion:
# nodejs prefixes its version strings with "v"
version = LooseVersion(version_str.lstrip('v'))
return version >= minversion
return True
except (subprocess.CalledProcessError, OSError):
return False
def get_project_root():
global project_root
return project_root
def get_eslint_module_path():
return os.path.join(get_project_root(), "tools", "lint", "eslint")
def lint(paths, binary=None, fix=None, setup=None, **lintargs):
"""Run eslint."""
global project_root
project_root = lintargs['root']
module_path = get_eslint_module_path()
# eslint requires at least node 4.2.3
node_path = get_node_or_npm_path("node", LooseVersion("4.2.3"))
if not node_path:
return 1
if setup:
return eslint_setup()
npm_path = get_node_or_npm_path("npm")
if not npm_path:
return 1
if eslint_module_has_issues():
eslint_setup()
# Valid binaries are:
# - Any provided by the binary argument.
# - Any pointed at by the ESLINT environmental variable.
# - Those provided by mach eslint --setup.
#
# eslint --setup installs some mozilla specific plugins and installs
# all node modules locally. This is the preferred method of
# installation.
if not binary:
binary = os.environ.get('ESLINT', None)
if not binary:
binary = os.path.join(module_path, "node_modules", ".bin", "eslint")
if not os.path.isfile(binary):
binary = None
if not binary:
print(ESLINT_NOT_FOUND_MESSAGE)
return 1
extra_args = lintargs.get('extra_args') or []
cmd_args = [binary,
# Enable the HTML plugin.
# We can't currently enable this in the global config file
# because it has bad interactions with the SublimeText
# ESLint plugin (bug 1229874).
'--plugin', 'html',
# This keeps ext as a single argument.
'--ext', '[{}]'.format(','.join(EXTENSIONS)),
'--format', 'json',
] + extra_args + paths
# eslint requires that --fix be set before the --ext argument.
if fix:
cmd_args.insert(1, '--fix')
shell = False
if os.environ.get('MSYSTEM') in ('MINGW32', 'MINGW64'):
# The eslint binary needs to be run from a shell with msys
shell = True
orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
proc = ProcessHandler(cmd_args, env=os.environ, stream=None, shell=shell)
proc.run()
signal.signal(signal.SIGINT, orig)
try:
proc.wait()
except KeyboardInterrupt:
proc.kill()
return []
if not proc.output:
return [] # no output means success
try:
jsonresult = json.loads(proc.output[0])
except ValueError:
print(ESLINT_ERROR_MESSAGE.format("\n".join(proc.output)))
return 1
results = []
for obj in jsonresult:
errors = obj['messages']
for err in errors:
err.update({
'hint': err.get('fix'),
'level': 'error' if err['severity'] == 2 else 'warning',
'lineno': err.get('line'),
'path': obj['filePath'],
'rule': err.get('ruleId'),
})
results.append(result.from_linter(LINTER, **err))
return results
LINTER = {
'name': "eslint",
'description': "JavaScript linter",
# ESLint infra handles its own path filtering, so just include cwd
'include': ['.'],
'exclude': [],
'extensions': EXTENSIONS,
'type': 'external',
'payload': lint,
}

View file

@ -0,0 +1,363 @@
Mozilla Public License, version 2.0
1. Definitions
1.1. "Contributor"
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. "Incompatible With Secondary Licenses"
means
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the terms of
a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in a
separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible, whether
at the time of the initial grant or subsequently, any and all of the
rights conveyed by this License.
1.10. "Modifications"
means any of the following:
a. any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the License,
by the making, using, selling, offering for sale, having made, import,
or transfer of either its Contributions or its Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, "control" means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
a. for any code that a Contributor has removed from Covered Software; or
b. for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
c. under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights to
grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter the
recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty, or
limitations of liability) contained within the Source Code Form of the
Covered Software, except that You may alter any license notices to the
extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute,
judicial order, or regulation then You must: (a) comply with the terms of
this License to the maximum extent possible; and (b) describe the
limitations and the code they affect. Such description must be placed in a
text file included with all distributions of the Covered Software under
this License. Except to the extent prohibited by statute or regulation,
such description must be sufficiently detailed for a recipient of ordinary
skill to be able to understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing
basis, if such Contributor fails to notify You of the non-compliance by
some reasonable means prior to 60 days after You have come back into
compliance. Moreover, Your grants from a particular Contributor are
reinstated on an ongoing basis if such Contributor notifies You of the
non-compliance by some reasonable means, this is the first time You have
received notice of non-compliance with this License from such
Contributor, and You become compliant prior to 30 days after Your receipt
of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an "as is" basis,
without warranty of any kind, either expressed, implied, or statutory,
including, without limitation, warranties that the Covered Software is free
of defects, merchantable, fit for a particular purpose or non-infringing.
The entire risk as to the quality and performance of the Covered Software
is with You. Should any Covered Software prove defective in any respect,
You (not any Contributor) assume the cost of any necessary servicing,
repair, or correction. This disclaimer of warranty constitutes an essential
part of this License. No use of any Covered Software is authorized under
this License except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from
such party's negligence to the extent applicable law prohibits such
limitation. Some jurisdictions do not allow the exclusion or limitation of
incidental or consequential damages, so this exclusion and limitation may
not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts
of a jurisdiction where the defendant maintains its principal place of
business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions. Nothing
in this Section shall prevent a party's ability to bring cross-claims or
counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides that
the language of a contract shall be construed against the drafter shall not
be used to construe this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses If You choose to distribute Source Code Form that is
Incompatible With Secondary Licenses under the terms of this version of
the License, the notice described in Exhibit B of this License must be
attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a
notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible
With Secondary Licenses", as defined by
the Mozilla Public License, v. 2.0.

View file

@ -0,0 +1,188 @@
/**
* @fileoverview functions for scanning an AST for globals including
* traversing referenced scripts.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
const path = require("path");
const fs = require("fs");
const helpers = require("./helpers");
const escope = require("escope");
const estraverse = require("estraverse");
/**
* Parses a list of "name:boolean_value" or/and "name" options divided by comma or
* whitespace.
*
* This function was copied from eslint.js
*
* @param {string} string The string to parse.
* @param {Comment} comment The comment node which has the string.
* @returns {Object} Result map object of names and boolean values
*/
function parseBooleanConfig(string, comment) {
let items = {};
// Collapse whitespace around : to make parsing easier
string = string.replace(/\s*:\s*/g, ":");
// Collapse whitespace around ,
string = string.replace(/\s*,\s*/g, ",");
string.split(/\s|,+/).forEach(function(name) {
if (!name) {
return;
}
let pos = name.indexOf(":");
let value = undefined;
if (pos !== -1) {
value = name.substring(pos + 1, name.length);
name = name.substring(0, pos);
}
items[name] = {
value: (value === "true"),
comment: comment
};
});
return items;
}
/**
* Global discovery can require parsing many files. This map of
* {String} => {Object} caches what globals were discovered for a file path.
*/
const globalCache = new Map();
/**
* An object that returns found globals for given AST node types. Each prototype
* property should be named for a node type and accepts a node parameter and a
* parents parameter which is a list of the parent nodes of the current node.
* Each returns an array of globals found.
*
* @param {String} path
* The absolute path of the file being parsed.
*/
function GlobalsForNode(path) {
this.path = path;
this.root = helpers.getRootDir(path);
}
GlobalsForNode.prototype = {
BlockComment(node, parents) {
let value = node.value.trim();
let match = /^import-globals-from\s+(.+)$/.exec(value);
if (!match) {
return [];
}
let filePath = match[1].trim();
if (!path.isAbsolute(filePath)) {
let dirName = path.dirname(this.path);
filePath = path.resolve(dirName, filePath);
}
return module.exports.getGlobalsForFile(filePath);
},
ExpressionStatement(node, parents) {
let isGlobal = helpers.getIsGlobalScope(parents);
let names = helpers.convertExpressionToGlobals(node, isGlobal, this.root);
return names.map(name => { return { name, writable: true }});
},
};
module.exports = {
/**
* Returns all globals for a given file. Recursively searches through
* import-globals-from directives and also includes globals defined by
* standard eslint directives.
*
* @param {String} path
* The absolute path of the file to be parsed.
*/
getGlobalsForFile(path) {
if (globalCache.has(path)) {
return globalCache.get(path);
}
let content = fs.readFileSync(path, "utf8");
// Parse the content into an AST
let ast = helpers.getAST(content);
// Discover global declarations
let scopeManager = escope.analyze(ast);
let globalScope = scopeManager.acquire(ast);
let globals = Object.keys(globalScope.variables).map(v => ({
name: globalScope.variables[v].name,
writable: true,
}));
// Walk over the AST to find any of our custom globals
let handler = new GlobalsForNode(path);
helpers.walkAST(ast, (type, node, parents) => {
// We have to discover any globals that ESLint would have defined through
// comment directives
if (type == "BlockComment") {
let value = node.value.trim();
let match = /^globals?\s+(.+)$/.exec(value);
if (match) {
let values = parseBooleanConfig(match[1].trim(), node);
for (let name of Object.keys(values)) {
globals.push({
name,
writable: values[name].value
})
}
}
}
if (type in handler) {
let newGlobals = handler[type](node, parents);
globals.push.apply(globals, newGlobals);
}
});
globalCache.set(path, globals);
return globals;
},
/**
* Intended to be used as-is for an ESLint rule that parses for globals in
* the current file and recurses through import-globals-from directives.
*
* @param {Object} context
* The ESLint parsing context.
*/
getESLintGlobalParser(context) {
let globalScope;
let parser = {
Program(node) {
globalScope = context.getScope();
}
};
// Install thin wrappers around GlobalsForNode
let handler = new GlobalsForNode(helpers.getAbsoluteFilePath(context));
for (let type of Object.keys(GlobalsForNode.prototype)) {
parser[type] = function(node) {
let globals = handler[type](node, context.getAncestors());
helpers.addGlobals(globals, globalScope);
}
}
return parser;
}
};

View file

@ -0,0 +1,524 @@
/**
* @fileoverview A collection of helper functions.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
var escope = require("escope");
var espree = require("espree");
var estraverse = require("estraverse");
var path = require("path");
var fs = require("fs");
var ini = require("ini-parser");
var modules = null;
var directoryManifests = new Map();
var definitions = [
/^loader\.lazyGetter\(this, "(\w+)"/,
/^loader\.lazyImporter\(this, "(\w+)"/,
/^loader\.lazyServiceGetter\(this, "(\w+)"/,
/^loader\.lazyRequireGetter\(this, "(\w+)"/,
/^XPCOMUtils\.defineLazyGetter\(this, "(\w+)"/,
/^XPCOMUtils\.defineLazyModuleGetter\(this, "(\w+)"/,
/^XPCOMUtils\.defineLazyServiceGetter\(this, "(\w+)"/,
/^XPCOMUtils\.defineConstant\(this, "(\w+)"/,
/^DevToolsUtils\.defineLazyModuleGetter\(this, "(\w+)"/,
/^DevToolsUtils\.defineLazyGetter\(this, "(\w+)"/,
/^Object\.defineProperty\(this, "(\w+)"/,
/^Reflect\.defineProperty\(this, "(\w+)"/,
/^this\.__defineGetter__\("(\w+)"/,
/^this\.(\w+) =/
];
var imports = [
/^(?:Cu|Components\.utils)\.import\(".*\/((.*?)\.jsm?)"(?:, this)?\)/,
];
module.exports = {
/**
* Gets the abstract syntax tree (AST) of the JavaScript source code contained
* in sourceText.
*
* @param {String} sourceText
* Text containing valid JavaScript.
*
* @return {Object}
* The resulting AST.
*/
getAST: function(sourceText) {
// Use a permissive config file to allow parsing of anything that Espree
// can parse.
var config = this.getPermissiveConfig();
return espree.parse(sourceText, config);
},
/**
* A simplistic conversion of some AST nodes to a standard string form.
*
* @param {Object} node
* The AST node to convert.
*
* @return {String}
* The JS source for the node.
*/
getASTSource: function(node) {
switch (node.type) {
case "MemberExpression":
if (node.computed)
throw new Error("getASTSource unsupported computed MemberExpression");
return this.getASTSource(node.object) + "." + this.getASTSource(node.property);
case "ThisExpression":
return "this";
case "Identifier":
return node.name;
case "Literal":
return JSON.stringify(node.value);
case "CallExpression":
var args = node.arguments.map(a => this.getASTSource(a)).join(", ");
return this.getASTSource(node.callee) + "(" + args + ")";
case "ObjectExpression":
return "{}";
case "ExpressionStatement":
return this.getASTSource(node.expression) + ";";
case "FunctionExpression":
return "function() {}";
case "ArrowFunctionExpression":
return "() => {}";
case "AssignmentExpression":
return this.getASTSource(node.left) + " = " + this.getASTSource(node.right);
default:
throw new Error("getASTSource unsupported node type: " + node.type);
}
},
/**
* This walks an AST in a manner similar to ESLint passing node and comment
* events to the listener. The listener is expected to be a simple function
* which accepts node type, node and parents arguments.
*
* @param {Object} ast
* The AST to walk.
* @param {Function} listener
* A callback function to call for the nodes. Passed three arguments,
* event type, node and an array of parent nodes for the current node.
*/
walkAST(ast, listener) {
let parents = [];
let seenComments = new Set();
function sendCommentEvents(comments) {
if (!comments) {
return;
}
for (let comment of comments) {
if (seenComments.has(comment)) {
return;
}
seenComments.add(comment);
listener(comment.type + "Comment", comment, parents);
}
}
estraverse.traverse(ast, {
enter(node, parent) {
// Comments are held in node.comments for empty programs
let leadingComments = node.leadingComments;
if (node.type === "Program" && node.body.length == 0) {
leadingComments = node.comments;
}
sendCommentEvents(leadingComments);
listener(node.type, node, parents);
sendCommentEvents(node.trailingComments);
parents.push(node);
},
leave(node, parent) {
// TODO send comment exit events
listener(node.type + ":exit", node, parents);
if (parents.length == 0) {
throw new Error("Left more nodes than entered.");
}
parents.pop();
}
});
if (parents.length) {
throw new Error("Entered more nodes than left.");
}
},
/**
* Attempts to convert an ExpressionStatement to likely global variable
* definitions.
*
* @param {Object} node
* The AST node to convert.
* @param {boolean} isGlobal
* True if the current node is in the global scope.
* @param {String} repository
* The root of the repository.
*
* @return {Array}
* An array of variable names defined.
*/
convertExpressionToGlobals: function(node, isGlobal, repository) {
if (!modules) {
modules = require(path.join(repository, "tools", "lint", "eslint", "modules.json"));
}
try {
var source = this.getASTSource(node);
}
catch (e) {
return [];
}
for (var reg of definitions) {
var match = source.match(reg);
if (match) {
// Must be in the global scope
if (!isGlobal) {
return [];
}
return [match[1]];
}
}
for (reg of imports) {
var match = source.match(reg);
if (match) {
// The two argument form is only acceptable in the global scope
if (node.expression.arguments.length > 1 && !isGlobal) {
return [];
}
if (match[1] in modules) {
return modules[match[1]];
}
return [match[2]];
}
}
return [];
},
/**
* Add a variable to the current scope.
* HACK: This relies on eslint internals so it could break at any time.
*
* @param {String} name
* The variable name to add to the scope.
* @param {ASTScope} scope
* The scope to add to.
* @param {boolean} writable
* Whether the global can be overwritten.
*/
addVarToScope: function(name, scope, writable) {
scope.__defineGeneric(name, scope.set, scope.variables, null, null);
let variable = scope.set.get(name);
variable.eslintExplicitGlobal = false;
variable.writeable = writable;
// Walk to the global scope which holds all undeclared variables.
while (scope.type != "global") {
scope = scope.upper;
}
// "through" contains all references with no found definition.
scope.through = scope.through.filter(function(reference) {
if (reference.identifier.name != name) {
return true;
}
// Links the variable and the reference.
// And this reference is removed from `Scope#through`.
reference.resolved = variable;
variable.references.push(reference);
return false;
});
},
/**
* Adds a set of globals to a scope.
*
* @param {Array} globalVars
* An array of global variable names.
* @param {ASTScope} scope
* The scope.
*/
addGlobals: function(globalVars, scope) {
globalVars.forEach(v => this.addVarToScope(v.name, scope, v.writable));
},
/**
* To allow espree to parse almost any JavaScript we need as many features as
* possible turned on. This method returns that config.
*
* @return {Object}
* Espree compatible permissive config.
*/
getPermissiveConfig: function() {
return {
range: true,
loc: true,
comment: true,
attachComment: true,
ecmaVersion: 8,
sourceType: "script",
ecmaFeatures: {
experimentalObjectRestSpread: true,
globalReturn: true,
}
};
},
/**
* Check whether the context is the global scope.
*
* @param {Array} ancestors
* The parents of the current node.
*
* @return {Boolean}
* True or false
*/
getIsGlobalScope: function(ancestors) {
for (let parent of ancestors) {
if (parent.type == "FunctionExpression" ||
parent.type == "FunctionDeclaration") {
return false;
}
}
return true;
},
/**
* Check whether we might be in a test head file.
*
* @param {RuleContext} scope
* You should pass this from within a rule
* e.g. helpers.getIsHeadFile(this)
*
* @return {Boolean}
* True or false
*/
getIsHeadFile: function(scope) {
var pathAndFilename = this.cleanUpPath(scope.getFilename());
return /.*[\\/]head(_.+)?\.js$/.test(pathAndFilename);
},
/**
* Gets the head files for a potential test file
*
* @param {RuleContext} scope
* You should pass this from within a rule
* e.g. helpers.getIsHeadFile(this)
*
* @return {String[]}
* Paths to head files to load for the test
*/
getTestHeadFiles: function(scope) {
if (!this.getIsTest(scope)) {
return [];
}
let filepath = this.cleanUpPath(scope.getFilename());
let dir = path.dirname(filepath);
let names = fs.readdirSync(dir)
.filter(name => name.startsWith("head") && name.endsWith(".js"))
.map(name => path.join(dir, name));
return names;
},
/**
* Gets all the test manifest data for a directory
*
* @param {String} dir
* The directory
*
* @return {Array}
* An array of objects with file and manifest properties
*/
getManifestsForDirectory: function(dir) {
if (directoryManifests.has(dir)) {
return directoryManifests.get(dir);
}
let manifests = [];
let names = fs.readdirSync(dir);
for (let name of names) {
if (!name.endsWith(".ini")) {
continue;
}
try {
let manifest = ini.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
manifests.push({
file: path.join(dir, name),
manifest
})
} catch (e) {
}
}
directoryManifests.set(dir, manifests);
return manifests;
},
/**
* Gets the manifest file a test is listed in
*
* @param {RuleContext} scope
* You should pass this from within a rule
* e.g. helpers.getIsHeadFile(this)
*
* @return {String}
* The path to the test manifest file
*/
getTestManifest: function(scope) {
let filepath = this.cleanUpPath(scope.getFilename());
let dir = path.dirname(filepath);
let filename = path.basename(filepath);
for (let manifest of this.getManifestsForDirectory(dir)) {
if (filename in manifest.manifest) {
return manifest.file;
}
}
return null;
},
/**
* Check whether we are in a test of some kind.
*
* @param {RuleContext} scope
* You should pass this from within a rule
* e.g. helpers.getIsTest(this)
*
* @return {Boolean}
* True or false
*/
getIsTest: function(scope) {
// Regardless of the manifest name being in a manifest means we're a test.
let manifest = this.getTestManifest(scope);
if (manifest) {
return true;
}
return !!this.getTestType(scope);
},
/**
* Gets the type of test or null if this isn't a test.
*
* @param {RuleContext} scope
* You should pass this from within a rule
* e.g. helpers.getIsHeadFile(this)
*
* @return {String or null}
* Test type: xpcshell, browser, chrome, mochitest
*/
getTestType: function(scope) {
let manifest = this.getTestManifest(scope);
if (manifest) {
let name = path.basename(manifest);
for (let testType of ["browser", "xpcshell", "chrome", "mochitest"]) {
if (name.startsWith(testType)) {
return testType;
}
}
}
let filepath = this.cleanUpPath(scope.getFilename());
let filename = path.basename(filepath);
if (filename.startsWith("browser_")) {
return "browser";
}
if (filename.startsWith("test_")) {
return "xpcshell";
}
return null;
},
/**
* Gets the root directory of the repository by walking up directories until
* a .eslintignore file is found.
* @param {String} fileName
* The absolute path of a file in the repository
*
* @return {String} The absolute path of the repository directory
*/
getRootDir: function(fileName) {
var dirName = path.dirname(fileName);
while (dirName && !fs.existsSync(path.join(dirName, ".eslintignore"))) {
dirName = path.dirname(dirName);
}
if (!dirName) {
throw new Error("Unable to find root of repository");
}
return dirName;
},
/**
* ESLint may be executed from various places: from mach, at the root of the
* repository, or from a directory in the repository when, for instance,
* executed by a text editor's plugin.
* The value returned by context.getFileName() varies because of this.
* This helper function makes sure to return an absolute file path for the
* current context, by looking at process.cwd().
* @param {Context} context
* @return {String} The absolute path
*/
getAbsoluteFilePath: function(context) {
var fileName = this.cleanUpPath(context.getFilename());
var cwd = process.cwd();
if (path.isAbsolute(fileName)) {
// Case 2: executed from the repo's root with mach:
// fileName: /path/to/mozilla/repo/a/b/c/d.js
// cwd: /path/to/mozilla/repo
return fileName;
} else if (path.basename(fileName) == fileName) {
// Case 1b: executed from a nested directory, fileName is the base name
// without any path info (happens in Atom with linter-eslint)
return path.join(cwd, fileName);
} else {
// Case 1: executed form in a nested directory, e.g. from a text editor:
// fileName: a/b/c/d.js
// cwd: /path/to/mozilla/repo/a/b/c
var dirName = path.dirname(fileName);
return cwd.slice(0, cwd.length - dirName.length) + fileName;
}
},
/**
* When ESLint is run from SublimeText, paths retrieved from
* context.getFileName contain leading and trailing double-quote characters.
* These characters need to be removed.
*/
cleanUpPath: function(path) {
return path.replace(/^"/, "").replace(/"$/, "");
}
};

View file

@ -0,0 +1,45 @@
/**
* @fileoverview A collection of rules that help enforce JavaScript coding
* standard and avoid common errors in the Mozilla project.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
//------------------------------------------------------------------------------
// Plugin Definition
//------------------------------------------------------------------------------
module.exports = {
processors: {
".xml": require("../lib/processors/xbl-bindings"),
},
rules: {
"balanced-listeners": require("../lib/rules/balanced-listeners"),
"import-globals": require("../lib/rules/import-globals"),
"import-headjs-globals": require("../lib/rules/import-headjs-globals"),
"import-browserjs-globals": require("../lib/rules/import-browserjs-globals"),
"mark-test-function-used": require("../lib/rules/mark-test-function-used"),
"no-aArgs": require("../lib/rules/no-aArgs"),
"no-cpows-in-tests": require("../lib/rules/no-cpows-in-tests"),
"no-single-arg-cu-import": require("../lib/rules/no-single-arg-cu-import"),
"reject-importGlobalProperties": require("../lib/rules/reject-importGlobalProperties"),
"reject-some-requires": require("../lib/rules/reject-some-requires"),
"var-only-at-top-level": require("../lib/rules/var-only-at-top-level")
},
rulesConfig: {
"balanced-listeners": 0,
"import-globals": 0,
"import-headjs-globals": 0,
"import-browserjs-globals": 0,
"mark-test-function-used": 0,
"no-aArgs": 0,
"no-cpows-in-tests": 0,
"no-single-arg-cu-import": 0,
"reject-importGlobalProperties": 0,
"reject-some-requires": 0,
"var-only-at-top-level": 0
}
};

View file

@ -0,0 +1,363 @@
/**
* @fileoverview Converts functions and handlers from XBL bindings into JS
* functions
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
const NS_XBL = "http://www.mozilla.org/xbl";
let sax = require("sax");
// Converts sax's error message to something that eslint will understand
let errorRegex = /(.*)\nLine: (\d+)\nColumn: (\d+)\nChar: (.*)/
function parseError(err) {
let matches = err.message.match(errorRegex);
if (!matches)
return null;
return {
fatal: true,
message: matches[1],
line: parseInt(matches[2]) + 1,
column: parseInt(matches[3])
}
}
let entityRegex = /&[\w][\w-\.]*;/g;
// A simple sax listener that generates a tree of element information
function XMLParser(parser) {
this.parser = parser;
parser.onopentag = this.onOpenTag.bind(this);
parser.onclosetag = this.onCloseTag.bind(this);
parser.ontext = this.onText.bind(this);
parser.onopencdata = this.onOpenCDATA.bind(this);
parser.oncdata = this.onCDATA.bind(this);
parser.oncomment = this.onComment.bind(this);
this.document = {
local: "#document",
uri: null,
children: [],
comments: [],
}
this._currentNode = this.document;
}
XMLParser.prototype = {
parser: null,
onOpenTag: function(tag) {
let node = {
parentNode: this._currentNode,
local: tag.local,
namespace: tag.uri,
attributes: {},
children: [],
comments: [],
textContent: "",
textLine: this.parser.line,
textColumn: this.parser.column,
textEndLine: this.parser.line
}
for (let attr of Object.keys(tag.attributes)) {
if (tag.attributes[attr].uri == "") {
node.attributes[attr] = tag.attributes[attr].value;
}
}
this._currentNode.children.push(node);
this._currentNode = node;
},
onCloseTag: function(tagname) {
this._currentNode.textEndLine = this.parser.line;
this._currentNode = this._currentNode.parentNode;
},
addText: function(text) {
this._currentNode.textContent += text;
},
onText: function(text) {
// Replace entities with some valid JS token.
this.addText(text.replace(entityRegex, "null"));
},
onOpenCDATA: function() {
// Turn the CDATA opening tag into whitespace for indent alignment
this.addText(" ".repeat("<![CDATA[".length));
},
onCDATA: function(text) {
this.addText(text);
},
onComment: function(text) {
this._currentNode.comments.push(text);
}
}
// -----------------------------------------------------------------------------
// Processor Definition
// -----------------------------------------------------------------------------
const INDENT_LEVEL = 2;
function indent(count) {
return " ".repeat(count * INDENT_LEVEL);
}
// Stores any XML parse error
let xmlParseError = null;
// Stores the lines of JS code generated from the XBL
let scriptLines = [];
// Stores a map from the synthetic line number to the real line number
// and column offset.
let lineMap = [];
function addSyntheticLine(line, linePos, addDisableLine) {
lineMap[scriptLines.length] = { line: linePos, offset: null };
scriptLines.push(line + (addDisableLine ? "" : " // eslint-disable-line"));
}
/**
* Adds generated lines from an XBL node to the script to be passed back to eslint.
*/
function addNodeLines(node, reindent) {
let lines = node.textContent.split("\n");
let startLine = node.textLine;
let startColumn = node.textColumn;
// The case where there are no whitespace lines before the first text is
// treated differently for indentation
let indentFirst = false;
// Strip off any preceeding whitespace only lines. These are often used to
// format the XML and CDATA blocks.
while (lines.length && lines[0].trim() == "") {
indentFirst = true;
startLine++;
lines.shift();
}
// Strip off any whitespace lines at the end. These are often used to line
// up the closing tags
while (lines.length && lines[lines.length - 1].trim() == "") {
lines.pop();
}
if (!indentFirst) {
let firstLine = lines.shift();
firstLine = " ".repeat(reindent * INDENT_LEVEL) + firstLine;
// ESLint counts columns starting at 1 rather than 0
lineMap[scriptLines.length] = { line: startLine, offset: reindent * INDENT_LEVEL - (startColumn - 1) };
scriptLines.push(firstLine);
startLine++;
}
// Find the preceeding whitespace for all lines that aren't entirely whitespace
let indents = lines.filter(s => s.trim().length > 0)
.map(s => s.length - s.trimLeft().length);
// Find the smallest indent level in use
let minIndent = Math.min.apply(null, indents);
for (let line of lines) {
if (line.trim().length == 0) {
// Don't offset lines that are only whitespace, the only possible JS error
// is trailing whitespace and we want it to point at the right place
lineMap[scriptLines.length] = { line: startLine, offset: 0 };
} else {
line = " ".repeat(reindent * INDENT_LEVEL) + line.substring(minIndent);
lineMap[scriptLines.length] = { line: startLine, offset: reindent * INDENT_LEVEL - (minIndent - 1) };
}
scriptLines.push(line);
startLine++;
}
}
module.exports = {
preprocess: function(text, filename) {
xmlParseError = null;
scriptLines = [];
lineMap = [];
// Non-strict allows us to ignore many errors from entities and
// preprocessing at the expense of failing to report some XML errors.
// Unfortunately it also throws away the case of tagnames and attributes
let parser = sax.parser(false, {
lowercase: true,
xmlns: true,
});
parser.onerror = function(err) {
xmlParseError = parseError(err);
}
let xp = new XMLParser(parser);
parser.write(text);
// Sanity checks to make sure we're dealing with an XBL document
let document = xp.document;
if (document.children.length != 1) {
return [];
}
let bindings = document.children[0];
if (bindings.local != "bindings" || bindings.namespace != NS_XBL) {
return [];
}
for (let comment of document.comments) {
addSyntheticLine(`/*`, 0, true);
for (let line of comment.split("\n")) {
addSyntheticLine(`${line.trim()}`, 0, true);
}
addSyntheticLine(`*/`, 0, true);
}
addSyntheticLine(`this.bindings = {`, bindings.textLine);
for (let binding of bindings.children) {
if (binding.local != "binding" || binding.namespace != NS_XBL) {
continue;
}
addSyntheticLine(indent(1) + `"${binding.attributes.id}": {`, binding.textLine);
for (let part of binding.children) {
if (part.namespace != NS_XBL) {
continue;
}
if (part.local == "implementation") {
addSyntheticLine(indent(2) + `implementation: {`, part.textLine);
} else if (part.local == "handlers") {
addSyntheticLine(indent(2) + `handlers: [`, part.textLine);
} else {
continue;
}
for (let item of part.children) {
if (item.namespace != NS_XBL) {
continue;
}
switch (item.local) {
case "field": {
// Fields are something like lazy getter functions
// Ignore empty fields
if (item.textContent.trim().length == 0) {
continue;
}
addSyntheticLine(indent(3) + `get ${item.attributes.name}() {`, item.textLine);
addSyntheticLine(indent(4) + `return (`, item.textLine);
// Remove trailing semicolons, as we are adding our own
item.textContent = item.textContent.replace(/;(?=\s*$)/, "");
addNodeLines(item, 5);
addSyntheticLine(indent(4) + `);`, item.textLine);
addSyntheticLine(indent(3) + `},`, item.textEndLine);
break;
}
case "constructor":
case "destructor": {
// Constructors and destructors become function declarations
addSyntheticLine(indent(3) + `${item.local}() {`, item.textLine);
addNodeLines(item, 4);
addSyntheticLine(indent(3) + `},`, item.textEndLine);
break;
}
case "method": {
// Methods become function declarations with the appropriate params
let params = item.children.filter(n => n.local == "parameter" && n.namespace == NS_XBL)
.map(n => n.attributes.name)
.join(", ");
let body = item.children.filter(n => n.local == "body" && n.namespace == NS_XBL)[0];
addSyntheticLine(indent(3) + `${item.attributes.name}(${params}) {`, item.textLine);
addNodeLines(body, 4);
addSyntheticLine(indent(3) + `},`, item.textEndLine);
break;
}
case "property": {
// Properties become one or two function declarations
for (let propdef of item.children) {
if (propdef.namespace != NS_XBL) {
continue;
}
if (propdef.local == "setter") {
addSyntheticLine(indent(3) + `set ${item.attributes.name}(val) {`, propdef.textLine);
} else if (propdef.local == "getter") {
addSyntheticLine(indent(3) + `get ${item.attributes.name}() {`, propdef.textLine);
} else {
continue;
}
addNodeLines(propdef, 4);
addSyntheticLine(indent(3) + `},`, propdef.textEndLine);
}
break;
}
case "handler": {
// Handlers become a function declaration with an `event` parameter
addSyntheticLine(indent(3) + `function(event) {`, item.textLine);
addNodeLines(item, 4);
addSyntheticLine(indent(3) + `},`, item.textEndLine);
break;
}
default:
continue;
}
}
addSyntheticLine(indent(2) + (part.local == "implementation" ? `},` : `],`), part.textEndLine);
}
addSyntheticLine(indent(1) + `},`, binding.textEndLine);
}
addSyntheticLine(`};`, bindings.textEndLine);
let script = scriptLines.join("\n") + "\n";
return [script];
},
postprocess: function(messages, filename) {
// If there was an XML parse error then just return that
if (xmlParseError) {
return [xmlParseError];
}
// For every message from every script block update the line to point to the
// correct place.
let errors = [];
for (let i = 0; i < messages.length; i++) {
for (let message of messages[i]) {
// ESLint indexes lines starting at 1 but our arrays start at 0
let mapped = lineMap[message.line - 1];
message.line = mapped.line + 1;
if (mapped.offset) {
message.column -= mapped.offset;
} else {
message.column = NaN;
}
errors.push(message);
}
}
return errors;
}
};

View file

@ -0,0 +1,51 @@
"use strict";
/**
* Based on npm coding standards at https://docs.npmjs.com/misc/coding-style.
*
* The places we differ from the npm coding style:
* - Commas should be at the end of a line.
* - Always use semicolons.
* - Functions should not have whitespace before params.
*/
module.exports = {
"env": {
"node": true
},
"rules": {
"brace-style": ["error", "1tbs"],
"camelcase": "error",
"comma-dangle": ["error", "never"],
"comma-spacing": "error",
"comma-style": ["error", "last"],
"curly": ["error", "multi-line"],
"handle-callback-err": ["error", "er"],
"indent": ["error", 2, {"SwitchCase": 1}],
"max-len": ["error", 80, "error"],
"no-multiple-empty-lines": ["error", {"max": 1}],
"no-undef": "error",
"no-undef-init": "error",
"no-unexpected-multiline": "error",
"object-curly-spacing": "off",
"one-var": ["error", "never"],
"operator-linebreak": ["error", "after"],
"semi": ["error", "always"],
"space-before-blocks": "error",
"space-before-function-paren": ["error", "never"],
"keyword-spacing": "error",
"strict": ["error", "global"],
},
// Globals accessible within node modules.
"globals": {
"DTRACE_HTTP_CLIENT_REQUEST": true,
"DTRACE_HTTP_CLIENT_RESPONSE": true,
"DTRACE_HTTP_SERVER_REQUEST": true,
"DTRACE_HTTP_SERVER_RESPONSE": true,
"DTRACE_NET_SERVER_CONNECTION": true,
"DTRACE_NET_STREAM_END": true,
"Intl": true,
},
};

View file

@ -0,0 +1,113 @@
/**
* @fileoverview Check that there's a removeEventListener for each
* addEventListener and an off for each on.
* Note that for now, this rule is rather simple in that it only checks that
* for each event name there is both an add and remove listener. It doesn't
* check that these are called on the right objects or with the same callback.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
var DICTIONARY = {
"addEventListener": "removeEventListener",
"on": "off"
};
// Invert this dictionary to make it easy later.
var INVERTED_DICTIONARY = {};
for (var i in DICTIONARY) {
INVERTED_DICTIONARY[DICTIONARY[i]] = i;
}
// Collect the add/remove listeners in these 2 arrays.
var addedListeners = [];
var removedListeners = [];
function addAddedListener(node) {
addedListeners.push({
functionName: node.callee.property.name,
type: node.arguments[0].value,
node: node.callee.property,
useCapture: node.arguments[2] ? node.arguments[2].value : null
});
}
function addRemovedListener(node) {
removedListeners.push({
functionName: node.callee.property.name,
type: node.arguments[0].value,
useCapture: node.arguments[2] ? node.arguments[2].value : null
});
}
function getUnbalancedListeners() {
var unbalanced = [];
for (var j = 0; j < addedListeners.length; j++) {
if (!hasRemovedListener(addedListeners[j])) {
unbalanced.push(addedListeners[j]);
}
}
addedListeners = removedListeners = [];
return unbalanced;
}
function hasRemovedListener(addedListener) {
for (var k = 0; k < removedListeners.length; k++) {
var listener = removedListeners[k];
if (DICTIONARY[addedListener.functionName] === listener.functionName &&
addedListener.type === listener.type &&
addedListener.useCapture === listener.useCapture) {
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
return {
CallExpression: function(node) {
if (node.arguments.length === 0) {
return;
}
if (node.callee.type === "MemberExpression") {
var listenerMethodName = node.callee.property.name;
if (DICTIONARY.hasOwnProperty(listenerMethodName)) {
addAddedListener(node);
} else if (INVERTED_DICTIONARY.hasOwnProperty(listenerMethodName)) {
addRemovedListener(node);
}
}
},
"Program:exit": function() {
getUnbalancedListeners().forEach(function(listener) {
context.report(listener.node,
"No corresponding '{{functionName}}({{type}})' was found.",
{
functionName: DICTIONARY[listener.functionName],
type: listener.type
});
});
}
};
};

View file

@ -0,0 +1,83 @@
/**
* @fileoverview Import globals from browser.js.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
var fs = require("fs");
var path = require("path");
var helpers = require("../helpers");
var globals = require("../globals");
const SCRIPTS = [
//"browser/base/content/nsContextMenu.js",
"toolkit/content/contentAreaUtils.js",
"browser/components/places/content/editBookmarkOverlay.js",
"toolkit/components/printing/content/printUtils.js",
"toolkit/content/viewZoomOverlay.js",
"browser/components/places/content/browserPlacesViews.js",
"browser/base/content/browser.js",
"browser/components/downloads/content/downloads.js",
"browser/components/downloads/content/indicator.js",
"browser/components/customizableui/content/panelUI.js",
"toolkit/components/viewsource/content/viewSourceUtils.js",
"browser/base/content/browser-addons.js",
"browser/base/content/browser-ctrlTab.js",
"browser/base/content/browser-customization.js",
"browser/base/content/browser-devedition.js",
"browser/base/content/browser-feeds.js",
"browser/base/content/browser-fullScreenAndPointerLock.js",
"browser/base/content/browser-fullZoom.js",
"browser/base/content/browser-gestureSupport.js",
"browser/base/content/browser-media.js",
"browser/base/content/browser-places.js",
"browser/base/content/browser-plugins.js",
"browser/base/content/browser-refreshblocker.js",
"browser/base/content/browser-safebrowsing.js",
"browser/base/content/browser-sidebar.js",
"browser/base/content/browser-social.js",
"browser/base/content/browser-syncui.js",
"browser/base/content/browser-tabsintitlebar.js",
"browser/base/content/browser-thumbnails.js",
"browser/base/content/browser-trackingprotection.js",
"browser/base/content/browser-data-submission-info-bar.js",
"browser/base/content/browser-fxaccounts.js"
];
module.exports = function(context) {
return {
Program: function(node) {
if (helpers.getTestType(this) != "browser" &&
!helpers.getIsHeadFile(this)) {
return;
}
let filepath = helpers.getAbsoluteFilePath(context);
let root = helpers.getRootDir(filepath);
for (let script of SCRIPTS) {
let fileName = path.join(root, script);
try {
let newGlobals = globals.getGlobalsForFile(fileName);
helpers.addGlobals(newGlobals, context.getScope());
} catch (e) {
context.report(
node,
"Could not load globals from file {{filePath}}: {{error}}",
{
filePath: path.relative(root, fileName),
error: e
}
);
}
}
}
};
};

View file

@ -0,0 +1,15 @@
/**
* @fileoverview Discovers all globals for the current file.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = require("../globals").getESLintGlobalParser;

View file

@ -0,0 +1,49 @@
/**
* @fileoverview Import globals from head.js and from any files that were
* imported by head.js (as far as we can correctly resolve the path).
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
var fs = require("fs");
var path = require("path");
var helpers = require("../helpers");
var globals = require("../globals");
module.exports = function(context) {
function importHead(path, node) {
try {
let stats = fs.statSync(path);
if (!stats.isFile()) {
return;
}
} catch (e) {
return;
}
let newGlobals = globals.getGlobalsForFile(path);
helpers.addGlobals(newGlobals, context.getScope());
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
return {
Program: function(node) {
let heads = helpers.getTestHeadFiles(this);
for (let head of heads) {
importHead(head, node);
}
}
};
};

View file

@ -0,0 +1,37 @@
/**
* @fileoverview Simply marks `test` (the test method) or `run_test` as used when
* in mochitests or xpcshell tests respectively. This avoids ESLint telling us
* that the function is never called.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
var helpers = require("../helpers");
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
return {
Program: function() {
if (helpers.getTestType(this) == "browser") {
context.markVariableAsUsed("test");
return;
}
if (helpers.getTestType(this) == "xpcshell") {
context.markVariableAsUsed("run_test");
return;
}
}
};
};

View file

@ -0,0 +1,55 @@
/**
* @fileoverview warns against using hungarian notation in function arguments
* (i.e. aArg).
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function isPrefixed(name) {
return name.length >= 2 && /^a[A-Z]/.test(name);
}
function deHungarianize(name) {
return name.substring(1, 2).toLowerCase() +
name.substring(2, name.length);
}
function checkFunction(node) {
for (var i = 0; i < node.params.length; i++) {
var param = node.params[i];
if (param.name && isPrefixed(param.name)) {
var errorObj = {
name: param.name,
suggestion: deHungarianize(param.name)
};
context.report(param,
"Parameter '{{name}}' uses Hungarian Notation, " +
"consider using '{{suggestion}}' instead.",
errorObj);
}
}
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
return {
"FunctionDeclaration": checkFunction,
"ArrowFunctionExpression": checkFunction,
"FunctionExpression": checkFunction
};
};

View file

@ -0,0 +1,112 @@
/**
* @fileoverview Prevent access to CPOWs in browser mochitests.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
var helpers = require("../helpers");
var cpows = [
/^gBrowser\.contentWindow/,
/^gBrowser\.contentDocument/,
/^gBrowser\.selectedBrowser.contentWindow/,
/^browser\.contentDocument/,
/^window\.content/
];
var isInContentTask = false;
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function showError(node, identifier) {
if (isInContentTask) {
return;
}
context.report({
node: node,
message: identifier +
" is a possible Cross Process Object Wrapper (CPOW)."
});
}
function isContentTask(node) {
return node &&
node.type === "MemberExpression" &&
node.property.type === "Identifier" &&
node.property.name === "spawn" &&
node.object.type === "Identifier" &&
node.object.name === "ContentTask";
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
return {
CallExpression: function(node) {
if (isContentTask(node.callee)) {
isInContentTask = true;
}
},
"CallExpression:exit": function(node) {
if (isContentTask(node.callee)) {
isInContentTask = false;
}
},
MemberExpression: function(node) {
if (helpers.getTestType(this) != "browser") {
return;
}
var expression = context.getSource(node);
// Only report a single CPOW error per node -- so if checking
// |cpows| reports one, don't report another below.
var someCpowFound = cpows.some(function(cpow) {
if (cpow.test(expression)) {
showError(node, expression);
return true;
}
return false;
});
if (!someCpowFound && helpers.getIsGlobalScope(context.getAncestors())) {
if (/^content\./.test(expression)) {
showError(node, expression);
return;
}
}
},
Identifier: function(node) {
if (helpers.getTestType(this) != "browser") {
return;
}
var expression = context.getSource(node);
if (expression == "content" || /^content\./.test(expression)) {
if (node.parent.type === "MemberExpression" &&
node.parent.object &&
node.parent.object.type === "Identifier" &&
node.parent.object.name != "content") {
return;
}
showError(node, expression);
return;
}
}
};
};

View file

@ -0,0 +1,39 @@
/**
* @fileoverview Reject use of single argument Cu.import
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
var helpers = require("../helpers");
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
"CallExpression": function(node) {
if (node.callee.type === "MemberExpression") {
let memexp = node.callee;
if (memexp.object.type === "Identifier" &&
// Only Cu, not Components.utils; see bug 1230369.
memexp.object.name === "Cu" &&
memexp.property.type === "Identifier" &&
memexp.property.name === "import" &&
node.arguments.length === 1) {
context.report(node, "Single argument Cu.import exposes new " +
"globals to all modules");
}
}
}
};
};

View file

@ -0,0 +1,37 @@
/**
* @fileoverview Reject use of Cu.importGlobalProperties
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
var helpers = require("../helpers");
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
"CallExpression": function(node) {
if (node.callee.type === "MemberExpression") {
let memexp = node.callee;
if (memexp.object.type === "Identifier" &&
// Only Cu, not Components.utils; see bug 1230369.
memexp.object.name === "Cu" &&
memexp.property.type === "Identifier" &&
memexp.property.name === "importGlobalProperties") {
context.report(node, "Unexpected call to Cu.importGlobalProperties");
}
}
}
};
};

View file

@ -0,0 +1,48 @@
/**
* @fileoverview Reject some uses of require.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
if (typeof(context.options[0]) !== "string") {
throw new Error("reject-some-requires expects a regexp");
}
const RX = new RegExp(context.options[0]);
const checkPath = function(node, path) {
if (RX.test(path)) {
context.report(node, `require(${path}) is not allowed`);
}
};
return {
"CallExpression": function(node) {
if (node.callee.type == "Identifier" &&
node.callee.name == "require" &&
node.arguments.length == 1 &&
node.arguments[0].type == "Literal") {
checkPath(node, node.arguments[0].value);
} else if (node.callee.type == "MemberExpression" &&
node.callee.property.type == "Identifier" &&
node.callee.property.name == "lazyRequireGetter" &&
node.arguments.length >= 3 &&
node.arguments[2].type == "Literal") {
checkPath(node, node.arguments[2].value);
}
}
};
};

View file

@ -0,0 +1,34 @@
/**
* @fileoverview Marks all var declarations that are not at the top level
* invalid.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
"use strict";
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
var helpers = require("../helpers");
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
"VariableDeclaration": function(node) {
if (node.kind === "var") {
if (helpers.getIsGlobalScope(context.getAncestors())) {
return;
}
context.report(node, "Unexpected var, use let or const instead.");
}
}
};
};

View file

@ -0,0 +1,29 @@
{
"name": "eslint-plugin-mozilla",
"version": "0.2.5",
"description": "A collection of rules that help enforce JavaScript coding standard in the Mozilla project.",
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin",
"mozilla",
"firefox"
],
"bugs": {
"url": "https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Developer%20Tools"
},
"homepage": "https://bugzilla.mozilla.org/show_bug.cgi?id=1203520",
"author": "Mike Ratcliffe",
"main": "lib/index.js",
"dependencies": {
"escope": "^3.6.0",
"espree": "^3.2.0",
"estraverse": "^4.2.0",
"ini-parser": "^0.0.2",
"sax": "^1.1.4"
},
"engines": {
"node": ">=0.10.0"
},
"license": "MPL-2.0"
}

View file

@ -0,0 +1,9 @@
[
{
"size": 2768586,
"visibility": "public",
"digest": "1b9a7c5b1ca8d7d2aeb803055129d1cb0fa0d17b90dd3cf852ea77d86d1b1d9d09f34007a71908074afef9ce1ab87972a5cf16a36952e1a6159f7abfc6fa15b3",
"algorithm": "sha512",
"filename": "eslint.tar.gz"
}
]

View file

@ -0,0 +1,247 @@
{
"AboutHome.jsm": ["AboutHomeUtils", "AboutHome"],
"AddonLogging.jsm": ["LogManager"],
"AddonManager.jsm": ["AddonManager", "AddonManagerPrivate"],
"addons.js": ["AddonsEngine", "AddonValidator"],
"addons.jsm": ["Addon", "STATE_ENABLED", "STATE_DISABLED"],
"addonsreconciler.js": ["AddonsReconciler", "CHANGE_INSTALLED", "CHANGE_UNINSTALLED", "CHANGE_ENABLED", "CHANGE_DISABLED"],
"AddonTestUtils.jsm": ["AddonTestUtils", "MockAsyncShutdown"],
"addonutils.js": ["AddonUtils"],
"ajv-4.1.1.js": ["Ajv"],
"AlertsHelper.jsm": [],
"AppData.jsm": ["makeFakeAppDir"],
"AppInfo.jsm": ["newAppInfo", "getAppInfo", "updateAppInfo"],
"AppsServiceChild.jsm": ["DOMApplicationRegistry", "WrappedManifestCache"],
"arrays.js": ["inArray", "getSet", "indexOf", "remove", "rindexOf", "compare"],
"assertions.js": ["Assert", "Expect"],
"async.js": ["Async"],
"AsyncSpellCheckTestHelper.jsm": ["onSpellCheck"],
"Battery.jsm": ["GetBattery", "Battery"],
"blocklist-clients.js": ["AddonBlocklistClient", "GfxBlocklistClient", "OneCRLBlocklistClient", "PluginBlocklistClient", "FILENAME_ADDONS_JSON", "FILENAME_GFX_JSON", "FILENAME_PLUGINS_JSON"],
"blocklist-updater.js": ["checkVersions", "addTestBlocklistClient"],
"bogus_element_type.jsm": [],
"bookmark_validator.js": ["BookmarkValidator", "BookmarkProblemData"],
"bookmarks.js": ["BookmarksEngine", "PlacesItem", "Bookmark", "BookmarkFolder", "BookmarkQuery", "Livemark", "BookmarkSeparator"],
"bookmarks.jsm": ["PlacesItem", "Bookmark", "Separator", "Livemark", "BookmarkFolder", "DumpBookmarks"],
"BootstrapMonitor.jsm": ["monitor"],
"browser-loader.js": ["BrowserLoader"],
"browserid_identity.js": ["BrowserIDManager", "AuthenticationError"],
"CertUtils.jsm": ["BadCertHandler", "checkCert", "readCertPrefs", "validateCert"],
"clients.js": ["ClientEngine", "ClientsRec"],
"CloudSyncAdapters.jsm": ["Adapters"],
"CloudSyncBookmarks.jsm": ["Bookmarks"],
"CloudSyncBookmarksFolderCache.jsm": ["FolderCache"],
"CloudSyncEventSource.jsm": ["EventSource"],
"CloudSyncLocal.jsm": ["Local"],
"CloudSyncPlacesWrapper.jsm": ["PlacesWrapper"],
"CloudSyncTabs.jsm": ["Tabs"],
"cluster.js": ["ClusterManager"],
"collection_validator.js": ["CollectionValidator", "CollectionProblemData"],
"Console.jsm": ["console", "ConsoleAPI"],
"Constants.jsm": ["Roles", "Events", "Relations", "Filters", "States", "Prefilters"],
"ContactDB.jsm": ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME", "REVISION_STORE", "DB_VERSION"],
"content-server.jsm": ["init"],
"content.jsm": ["registerContentFrame"],
"ContentCrashHandlers.jsm": ["TabCrashHandler", "PluginCrashReporter", "UnsubmittedCrashHandler"],
"ContentObservers.jsm": [],
"ContentPrefUtils.jsm": ["ContentPref", "cbHandleResult", "cbHandleError", "cbHandleCompletion", "safeCallback"],
"controller.js": ["MozMillController", "globalEventRegistry", "sleep", "windowMap"],
"cookies.js": ["Cookies"],
"CoverageUtils.jsm": ["CoverageCollector"],
"CrashManagerTest.jsm": ["configureLogging", "getManager", "sleep", "TestingCrashManager"],
"dbg-client.jsm": ["DebuggerTransport", "DebuggerClient", "RootClient", "LongStringClient", "EnvironmentClient", "ObjectClient"],
"dbg-server.jsm": ["DebuggerServer", "ActorPool", "OriginalLocation"],
"debug.js": ["NS_ASSERT"],
"declined.js": ["DeclinedEngines"],
"dispatcher.js": ["Dispatcher"],
"distribution.js": ["DistributionCustomizer"],
"DNSTypes.jsm": ["DNS_QUERY_RESPONSE_CODES", "DNS_AUTHORITATIVE_ANSWER_CODES", "DNS_CLASS_CODES", "DNS_RECORD_TYPES"],
"dom.js": ["getAttributes"],
"DOMRequestHelper.jsm": ["DOMRequestIpcHelper"],
"DownloadCore.jsm": ["Download", "DownloadSource", "DownloadTarget", "DownloadError", "DownloadSaver", "DownloadCopySaver", "DownloadLegacySaver", "DownloadPDFSaver"],
"DownloadList.jsm": ["DownloadList", "DownloadCombinedList", "DownloadSummary"],
"E10SAddonsRollout.jsm": ["isAddonPartOfE10SRollout"],
"elementslib.js": ["ID", "Link", "XPath", "Selector", "Name", "Anon", "AnonXPath", "Lookup", "_byID", "_byName", "_byAttrib", "_byAnonAttrib"],
"engines.js": ["EngineManager", "Engine", "SyncEngine", "Tracker", "Store", "Changeset"],
"enginesync.js": ["EngineSynchronizer"],
"errors.js": ["BaseError", "ApplicationQuitError", "AssertionError", "TimeoutError"],
"evaluate.js": ["evaluate", "sandbox", "Sandboxes"],
"event-emitter.js": ["EventEmitter"],
"EventUtils.js": ["disableNonTestMouseEvents", "sendMouseEvent", "sendChar", "sendString", "sendKey", "synthesizeMouse", "synthesizeTouch", "synthesizeMouseAtPoint", "synthesizeTouchAtPoint", "synthesizeMouseAtCenter", "synthesizeTouchAtCenter", "synthesizeWheel", "synthesizeKey", "synthesizeMouseExpectEvent", "synthesizeKeyExpectEvent", "synthesizeText", "synthesizeComposition", "synthesizeQuerySelectedText"],
"Extension.jsm": ["Extension", "ExtensionData"],
"ExtensionAPI.jsm": ["ExtensionAPI", "ExtensionAPIs"],
"ExtensionXPCShellUtils.jsm": ["ExtensionTestUtils"],
"fakeservices.js": ["FakeCryptoService", "FakeFilesystemService", "FakeGUIDService", "fakeSHA256HMAC"],
"file_expandosharing.jsm": ["checkFromJSM"],
"file_stringencoding.jsm": ["checkFromJSM"],
"file_url.jsm": ["checkFromJSM"],
"file_worker_url.jsm": ["checkFromJSM"],
"Finder.jsm": ["Finder", "GetClipboardSearchString"],
"FormAutofillContent.jsm": ["FormAutofillHandler"],
"forms.js": ["FormEngine", "FormRec", "FormValidator"],
"forms.jsm": ["FormData"],
"frame.js": ["Collector", "Runner", "events", "runTestFile", "log", "timers", "persisted", "shutdownApplication"],
"FrameScriptManager.jsm": ["getNewLoaderID"],
"fxa_utils.js": ["initializeIdentityWithTokenServerResponse"],
"fxaccounts.jsm": ["Authentication"],
"FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
"FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],
"FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"],
"FxAccountsPush.js": ["FxAccountsPushService"],
"FxAccountsStorage.jsm": ["FxAccountsStorageManagerCanStoreField", "FxAccountsStorageManager"],
"FxAccountsWebChannel.jsm": ["EnsureFxAccountsWebChannel"],
"FxaMigrator.jsm": ["fxaMigrator"],
"gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
"gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
"Geometry.jsm": ["Point", "Rect"],
"Gestures.jsm": ["GestureSettings", "GestureTracker"],
"GMPInstallManager.jsm": ["GMPInstallManager", "GMPExtractor", "GMPDownloader", "GMPAddon"],
"GMPProvider.jsm": [],
"GMPUtils.jsm": ["EME_ADOBE_ID", "GMP_PLUGIN_IDS", "GMPPrefs", "GMPUtils", "OPEN_H264_ID", "WIDEVINE_ID"],
"hawkclient.js": ["HawkClient"],
"hawkrequest.js": ["HAWKAuthenticatedRESTRequest", "deriveHawkCredentials"],
"HelperApps.jsm": ["App", "HelperApps"],
"history.js": ["HistoryEngine", "HistoryRec"],
"history.jsm": ["HistoryEntry", "DumpHistory"],
"Http.jsm": ["httpRequest", "percentEncode"],
"httpd.js": ["HTTP_400", "HTTP_401", "HTTP_402", "HTTP_403", "HTTP_404", "HTTP_405", "HTTP_406", "HTTP_407", "HTTP_408", "HTTP_409", "HTTP_410", "HTTP_411", "HTTP_412", "HTTP_413", "HTTP_414", "HTTP_415", "HTTP_417", "HTTP_500", "HTTP_501", "HTTP_502", "HTTP_503", "HTTP_504", "HTTP_505", "HttpError", "HttpServer"],
"identity.js": ["IdentityManager"],
"Identity.jsm": ["IdentityService"],
"IdentityUtils.jsm": ["checkDeprecated", "checkRenamed", "getRandomId", "objectCopy", "makeMessageObject"],
"import_module.jsm": ["MODULE_IMPORTED", "MODULE_URI", "SUBMODULE_IMPORTED", "same_scope", "SUBMODULE_IMPORTED_TO_SCOPE"],
"import_sub_module.jsm": ["SUBMODULE_IMPORTED", "test_obj"],
"InlineSpellChecker.jsm": ["InlineSpellChecker", "SpellCheckHelper"],
"JNI.jsm": ["JNI", "android_log"],
"jpakeclient.js": ["JPAKEClient", "SendCredentialsController"],
"Jsbeautify.jsm": ["jsBeautify"],
"jsdebugger.jsm": ["addDebuggerToGlobal"],
"json2.js": ["JSON"],
"keys.js": ["BulkKeyBundle", "SyncKeyBundle"],
"KeyValueParser.jsm": ["parseKeyValuePairsFromLines", "parseKeyValuePairs", "parseKeyValuePairsFromFile"],
"kinto-http-client.js": ["KintoHttpClient"],
"kinto-offline-client.js": ["loadKinto"],
"loader-plugin-raw.jsm": ["requireRawId"],
"loader.js": ["WorkerDebuggerLoader", "worker"],
"Loader.jsm": ["DevToolsLoader", "devtools", "BuiltinProvider", "require", "loader"],
"logger.jsm": ["Logger"],
"logging.js": ["getTestLogger", "initTestLogging"],
"LoginManagerContent.jsm": ["LoginManagerContent", "LoginFormFactory", "UserAutoCompleteResult"],
"LoginRecipes.jsm": ["LoginRecipesContent", "LoginRecipesParent"],
"logmanager.js": ["LogManager"],
"LogUtils.jsm": ["Logger"],
"lz4.js": ["Lz4"],
"lz4_internal.js": ["Primitives"],
"main.js": ["Weave"],
"MatchPattern.jsm": ["MatchPattern", "MatchGlobs", "MatchURLFilters"],
"mcc_iso3166_table.jsm": ["MCC_ISO3166_TABLE"],
"message.js": ["Command", "Message", "MessageOrigin", "Response"],
"Messaging.jsm": ["sendMessageToJava", "Messaging"],
"microformat-shiv.js": ["Microformats"],
"MigrationUtils.jsm": ["MigrationUtils", "MigratorPrototype"],
"MinimalIdentity.jsm": ["IdentityService"],
"mozelement.js": ["Elem", "Selector", "ID", "Link", "XPath", "Name", "Lookup", "MozMillElement", "MozMillCheckBox", "MozMillRadio", "MozMillDropList", "MozMillTextBox", "subclasses"],
"mozmill.js": ["controller", "utils", "elementslib", "os", "getBrowserController", "newBrowserController", "getAddonsController", "getPreferencesController", "newMail3PaneController", "getMail3PaneController", "wm", "platform", "getAddrbkController", "getMsgComposeController", "getDownloadsController", "Application", "findElement", "getPlacesController", "isMac", "isLinux", "isWindows", "firePythonCallback", "getAddons"],
"msgbroker.js": ["addListener", "addObject", "removeListener", "sendMessage", "log", "pass", "fail"],
"MulticastDNSAndroid.jsm": ["MulticastDNS"],
"NativeMessaging.jsm": ["HostManifestManager", "NativeApp"],
"NetworkPrioritizer.jsm": ["trackBrowserWindow"],
"NotificationDB.jsm": [],
"nsFormAutoCompleteResult.jsm": ["FormAutoCompleteResult"],
"objects.js": ["getLength"],
"observers.js": ["Observers"],
"offlineAppCache.jsm": ["OfflineAppCacheHelper"],
"OrientationChangeHandler.jsm": [],
"os.js": ["listDirectory", "getFileForPath", "abspath", "getPlatform"],
"osfile.jsm": ["OS"],
"osfile_async_front.jsm": ["OS"],
"osfile_native.jsm": ["read"],
"osfile_shared_allthreads.jsm": ["LOG", "clone", "Config", "Constants", "Type", "HollowStructure", "OSError", "Library", "declareFFI", "declareLazy", "declareLazyFFI", "normalizeBufferArgs", "projectValue", "isArrayBuffer", "isTypedArray", "defineLazyGetter", "OS"],
"osfile_unix_allthreads.jsm": ["declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry", "Type", "POS_START", "POS_CURRENT", "POS_END"],
"osfile_win_allthreads.jsm": ["declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry", "Type", "POS_START", "POS_CURRENT", "POS_END"],
"ospath_unix.jsm": ["basename", "dirname", "join", "normalize", "split", "toFileURI", "fromFileURI"],
"ospath_win.jsm": ["basename", "dirname", "join", "normalize", "split", "winGetDrive", "winIsAbsolute", "toFileURI", "fromFileURI"],
"OutputGenerator.jsm": ["UtteranceGenerator", "BrailleGenerator"],
"PageMenu.jsm": ["PageMenuParent", "PageMenuChild"],
"PageThumbs.jsm": ["PageThumbs", "PageThumbsStorage"],
"Parser.jsm": ["Parser", "ParserHelpers", "SyntaxTreeVisitor"],
"parsingTestHelpers.jsm": ["generateURIsFromDirTree"],
"passwords.js": ["PasswordEngine", "LoginRec", "PasswordValidator"],
"passwords.jsm": ["Password", "DumpPasswords"],
"PdfJsNetwork.jsm": ["NetworkManager"],
"PermissionSettings.jsm": ["PermissionSettingsModule"],
"PermissionsTable.jsm": ["PermissionsTable", "PermissionsReverseTable", "expandPermissions", "appendAccessToPermName", "isExplicitInPermissionsTable", "AllPossiblePermissions"],
"PhoneNumberMetaData.jsm": ["PHONE_NUMBER_META_DATA"],
"PlacesUtils.jsm": ["PlacesUtils", "PlacesAggregatedTransaction", "PlacesCreateFolderTransaction", "PlacesCreateBookmarkTransaction", "PlacesCreateSeparatorTransaction", "PlacesCreateLivemarkTransaction", "PlacesMoveItemTransaction", "PlacesRemoveItemTransaction", "PlacesEditItemTitleTransaction", "PlacesEditBookmarkURITransaction", "PlacesSetItemAnnotationTransaction", "PlacesSetPageAnnotationTransaction", "PlacesEditBookmarkKeywordTransaction", "PlacesEditBookmarkPostDataTransaction", "PlacesEditItemDateAddedTransaction", "PlacesEditItemLastModifiedTransaction", "PlacesSortFolderByNameTransaction", "PlacesTagURITransaction", "PlacesUntagURITransaction"],
"PluginProvider.jsm": [],
"PointerAdapter.jsm": ["PointerRelay", "PointerAdapter"],
"policies.js": ["ErrorHandler", "SyncScheduler"],
"prefs.js": ["PrefsEngine", "PrefRec"],
"prefs.jsm": ["Preference"],
"PresentationDeviceInfoManager.jsm": ["PresentationDeviceInfoService"],
"PromiseWorker.jsm": ["BasePromiseWorker"],
"PushCrypto.jsm": ["PushCrypto", "concatArray"],
"quit.js": ["goQuitApplication"],
"record.js": ["WBORecord", "RecordManager", "CryptoWrapper", "CollectionKeyManager", "Collection"],
"recursive_importA.jsm": ["foo", "bar"],
"recursive_importB.jsm": ["baz", "qux"],
"reflect.jsm": ["Reflect"],
"RemoteFinder.jsm": ["RemoteFinder", "RemoteFinderListener"],
"RemotePageManager.jsm": ["RemotePages", "RemotePageManager", "PageListener"],
"RemoteWebProgress.jsm": ["RemoteWebProgressManager"],
"resource.js": ["AsyncResource", "Resource"],
"responsivedesign.jsm": ["ResponsiveUIManager"],
"rest.js": ["RESTRequest", "RESTResponse", "TokenAuthenticatedRESTRequest", "SyncStorageRequest"],
"rotaryengine.js": ["RotaryEngine", "RotaryRecord", "RotaryStore", "RotaryTracker"],
"RTCStatsReport.jsm": ["convertToRTCStatsReport"],
"scratchpad-manager.jsm": ["ScratchpadManager"],
"server.js": ["MarionetteServer"],
"service.js": ["Service"],
"SettingsDB.jsm": ["SettingsDB", "SETTINGSDB_NAME", "SETTINGSSTORE_NAME"],
"SharedPromptUtils.jsm": ["PromptUtils", "EnableDelayHelper"],
"ShutdownLeaksCollector.jsm": ["ContentCollector"],
"SignInToWebsite.jsm": ["SignInToWebsiteController"],
"Social.jsm": ["Social", "OpenGraphBuilder", "DynamicResizeWatcher", "sizeSocialPanelToContent"],
"SpecialPowersObserver.jsm": ["SpecialPowersObserver", "SpecialPowersObserverFactory"],
"stack.js": ["findCallerFrame"],
"StateMachineHelper.jsm": ["State", "CommandType"],
"status.js": ["Status"],
"storageserver.js": ["ServerBSO", "StorageServerCallback", "StorageServerCollection", "StorageServer", "storageServerForUsers"],
"stringbundle.js": ["StringBundle"],
"strings.js": ["trim", "vslice"],
"StructuredLog.jsm": ["StructuredLogger", "StructuredFormatter"],
"StyleEditorUtil.jsm": ["getString", "assert", "log", "text", "wire", "showFilePicker"],
"subprocess_common.jsm": ["BaseProcess", "PromiseWorker", "SubprocessConstants"],
"subprocess_unix.jsm": ["SubprocessImpl"],
"subprocess_win.jsm": ["SubprocessImpl"],
"sync.jsm": ["Authentication"],
"tabs.js": ["TabEngine", "TabSetRecord"],
"tabs.jsm": ["BrowserTabs"],
"tcpsocket_test.jsm": ["createSocket", "createServer", "enablePrefsAndPermissions", "socketCompartmentInstanceOfArrayBuffer"],
"telemetry.js": ["SyncTelemetry"],
"test.jsm": ["Foo"],
"test2.jsm": ["Bar"],
"test_bug883784.jsm": ["Test"],
"Timer.jsm": ["setTimeout", "clearTimeout", "setInterval", "clearInterval"],
"tokenserverclient.js": ["TokenServerClient", "TokenServerClientError", "TokenServerClientNetworkError", "TokenServerClientServerError"],
"ToolboxProcess.jsm": ["BrowserToolboxProcess"],
"tps.jsm": ["ACTIONS", "TPS"],
"Translation.jsm": ["Translation", "TranslationTelemetry"],
"Traversal.jsm": ["TraversalRules", "TraversalHelper"],
"UpdateTelemetry.jsm": ["AUSTLMY"],
"userapi.js": ["UserAPI10Client"],
"util.js": ["getChromeWindow", "XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"],
"utils.js": ["applicationName", "assert", "Copy", "getBrowserObject", "getChromeWindow", "getWindows", "getWindowByTitle", "getWindowByType", "getWindowId", "getMethodInWindows", "getPreference", "saveDataURL", "setPreference", "sleep", "startTimer", "stopTimer", "takeScreenshot", "unwrapNode", "waitFor", "btoa", "encryptPayload", "isConfiguredWithLegacyIdentity", "ensureLegacyIdentityManager", "setBasicCredentials", "makeIdentityConfig", "makeFxAccountsInternalMock", "configureFxAccountIdentity", "configureIdentity", "SyncTestingInfrastructure", "waitForZeroTimer", "Promise", "add_identity_test", "MockFxaStorageManager", "AccountState", "sumHistogram", "CommonUtils", "CryptoUtils", "TestingUtils"],
"Utils.jsm": ["Utils", "Logger", "PivotContext", "PrefCache", "SettingCache"],
"VariablesView.jsm": ["VariablesView", "escapeHTML"],
"VariablesViewController.jsm": ["VariablesViewController", "StackFrameUtils"],
"version.jsm": ["VERSION"],
"vtt.jsm": ["WebVTT"],
"WebChannel.jsm": ["WebChannel", "WebChannelBroker"],
"WindowDraggingUtils.jsm": ["WindowDraggingElement"],
"windows.js": ["init", "map"],
"windows.jsm": ["BrowserWindows"],
"WindowsJumpLists.jsm": ["WinTaskbarJumpList"],
"WindowsPreviewPerTab.jsm": ["AeroPeek"],
"withs.js": ["startsWith", "endsWith"],
"xul-app.jsm": ["XulApp"]
}

718
tools/lint/eslint/npm-shrinkwrap.json generated Normal file
View file

@ -0,0 +1,718 @@
{
"name": "mach-eslint",
"dependencies": {
"acorn": {
"version": "4.0.3",
"from": "acorn@>=4.0.1 <5.0.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.3.tgz"
},
"acorn-jsx": {
"version": "3.0.1",
"from": "acorn-jsx@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
"dependencies": {
"acorn": {
"version": "3.3.0",
"from": "acorn@>=3.0.4 <4.0.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz"
}
}
},
"ajv": {
"version": "4.8.2",
"from": "ajv@>=4.7.0 <5.0.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-4.8.2.tgz"
},
"ajv-keywords": {
"version": "1.1.1",
"from": "ajv-keywords@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.1.1.tgz"
},
"ansi-escapes": {
"version": "1.4.0",
"from": "ansi-escapes@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz"
},
"ansi-regex": {
"version": "2.0.0",
"from": "ansi-regex@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
},
"ansi-styles": {
"version": "2.2.1",
"from": "ansi-styles@>=2.2.1 <3.0.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
},
"argparse": {
"version": "1.0.9",
"from": "argparse@>=1.0.7 <2.0.0",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz"
},
"array-union": {
"version": "1.0.2",
"from": "array-union@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz"
},
"array-uniq": {
"version": "1.0.3",
"from": "array-uniq@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz"
},
"arrify": {
"version": "1.0.1",
"from": "arrify@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz"
},
"balanced-match": {
"version": "0.4.2",
"from": "balanced-match@>=0.4.1 <0.5.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz"
},
"brace-expansion": {
"version": "1.1.6",
"from": "brace-expansion@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz"
},
"caller-path": {
"version": "0.1.0",
"from": "caller-path@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz"
},
"callsites": {
"version": "0.2.0",
"from": "callsites@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz"
},
"chalk": {
"version": "1.1.3",
"from": "chalk@>=1.1.3 <2.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz"
},
"circular-json": {
"version": "0.3.1",
"from": "circular-json@>=0.3.0 <0.4.0",
"resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz"
},
"cli-cursor": {
"version": "1.0.2",
"from": "cli-cursor@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz"
},
"cli-width": {
"version": "2.1.0",
"from": "cli-width@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz"
},
"co": {
"version": "4.6.0",
"from": "co@>=4.6.0 <5.0.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz"
},
"code-point-at": {
"version": "1.1.0",
"from": "code-point-at@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz"
},
"concat-map": {
"version": "0.0.1",
"from": "concat-map@0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
},
"concat-stream": {
"version": "1.5.2",
"from": "concat-stream@>=1.4.6 <2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz"
},
"core-util-is": {
"version": "1.0.2",
"from": "core-util-is@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
},
"d": {
"version": "0.1.1",
"from": "d@>=0.1.1 <0.2.0",
"resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz"
},
"debug": {
"version": "2.3.0",
"from": "debug@>=2.1.1 <3.0.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.3.0.tgz"
},
"deep-is": {
"version": "0.1.3",
"from": "deep-is@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz"
},
"del": {
"version": "2.2.2",
"from": "del@>=2.0.2 <3.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz"
},
"doctrine": {
"version": "1.5.0",
"from": "doctrine@>=1.2.2 <2.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz"
},
"dom-serializer": {
"version": "0.1.0",
"from": "dom-serializer@>=0.0.0 <1.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
"dependencies": {
"domelementtype": {
"version": "1.1.3",
"from": "domelementtype@>=1.1.1 <1.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz"
}
}
},
"domelementtype": {
"version": "1.3.0",
"from": "domelementtype@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz"
},
"domhandler": {
"version": "2.3.0",
"from": "domhandler@>=2.3.0 <3.0.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz"
},
"domutils": {
"version": "1.5.1",
"from": "domutils@>=1.5.1 <2.0.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz"
},
"entities": {
"version": "1.1.1",
"from": "entities@>=1.1.1 <2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz"
},
"es5-ext": {
"version": "0.10.12",
"from": "es5-ext@>=0.10.11 <0.11.0",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.12.tgz"
},
"es6-iterator": {
"version": "2.0.0",
"from": "es6-iterator@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.0.tgz"
},
"es6-map": {
"version": "0.1.4",
"from": "es6-map@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.4.tgz"
},
"es6-set": {
"version": "0.1.4",
"from": "es6-set@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.4.tgz"
},
"es6-symbol": {
"version": "3.1.0",
"from": "es6-symbol@>=3.1.0 <3.2.0",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.0.tgz"
},
"es6-weak-map": {
"version": "2.0.1",
"from": "es6-weak-map@>=2.0.1 <3.0.0",
"resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.1.tgz"
},
"escape-string-regexp": {
"version": "1.0.5",
"from": "escape-string-regexp@>=1.0.2 <2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
},
"escope": {
"version": "3.6.0",
"from": "escope@>=3.6.0 <4.0.0",
"resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz"
},
"eslint": {
"version": "3.8.1",
"from": "eslint@3.8.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-3.8.1.tgz"
},
"eslint-plugin-html": {
"version": "1.5.2",
"from": "eslint-plugin-html@1.5.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-1.5.2.tgz"
},
"eslint-plugin-react": {
"version": "4.2.3",
"from": "eslint-plugin-react@4.2.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-4.2.3.tgz"
},
"espree": {
"version": "3.3.2",
"from": "espree@>=3.2.0 <4.0.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.3.2.tgz"
},
"esprima": {
"version": "2.7.3",
"from": "esprima@>=2.6.0 <3.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz"
},
"esrecurse": {
"version": "4.1.0",
"from": "esrecurse@>=4.1.0 <5.0.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz",
"dependencies": {
"estraverse": {
"version": "4.1.1",
"from": "estraverse@>=4.1.0 <4.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz"
}
}
},
"estraverse": {
"version": "4.2.0",
"from": "estraverse@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz"
},
"esutils": {
"version": "2.0.2",
"from": "esutils@>=2.0.2 <3.0.0",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz"
},
"event-emitter": {
"version": "0.3.4",
"from": "event-emitter@>=0.3.4 <0.4.0",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.4.tgz"
},
"exit-hook": {
"version": "1.1.1",
"from": "exit-hook@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz"
},
"fast-levenshtein": {
"version": "2.0.5",
"from": "fast-levenshtein@>=2.0.4 <2.1.0",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz"
},
"figures": {
"version": "1.7.0",
"from": "figures@>=1.3.5 <2.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz"
},
"file-entry-cache": {
"version": "2.0.0",
"from": "file-entry-cache@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz"
},
"flat-cache": {
"version": "1.2.1",
"from": "flat-cache@>=1.2.1 <2.0.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.1.tgz"
},
"fs.realpath": {
"version": "1.0.0",
"from": "fs.realpath@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
},
"generate-function": {
"version": "2.0.0",
"from": "generate-function@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
},
"generate-object-property": {
"version": "1.2.0",
"from": "generate-object-property@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz"
},
"glob": {
"version": "7.1.1",
"from": "glob@>=7.0.3 <8.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz"
},
"globals": {
"version": "9.12.0",
"from": "globals@>=9.2.0 <10.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.12.0.tgz"
},
"globby": {
"version": "5.0.0",
"from": "globby@>=5.0.0 <6.0.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz"
},
"graceful-fs": {
"version": "4.1.10",
"from": "graceful-fs@>=4.1.2 <5.0.0",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.10.tgz"
},
"has-ansi": {
"version": "2.0.0",
"from": "has-ansi@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz"
},
"htmlparser2": {
"version": "3.9.2",
"from": "htmlparser2@>=3.8.2 <4.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz"
},
"ignore": {
"version": "3.2.0",
"from": "ignore@>=3.1.5 <4.0.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.2.0.tgz"
},
"imurmurhash": {
"version": "0.1.4",
"from": "imurmurhash@>=0.1.4 <0.2.0",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
},
"inflight": {
"version": "1.0.6",
"from": "inflight@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
},
"inherits": {
"version": "2.0.3",
"from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz"
},
"ini-parser": {
"version": "0.0.2",
"from": "ini-parser@>=0.0.2 <0.0.3",
"resolved": "https://registry.npmjs.org/ini-parser/-/ini-parser-0.0.2.tgz"
},
"inquirer": {
"version": "0.12.0",
"from": "inquirer@>=0.12.0 <0.13.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz"
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"from": "is-fullwidth-code-point@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz"
},
"is-my-json-valid": {
"version": "2.15.0",
"from": "is-my-json-valid@>=2.10.0 <3.0.0",
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz"
},
"is-path-cwd": {
"version": "1.0.0",
"from": "is-path-cwd@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz"
},
"is-path-in-cwd": {
"version": "1.0.0",
"from": "is-path-in-cwd@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz"
},
"is-path-inside": {
"version": "1.0.0",
"from": "is-path-inside@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz"
},
"is-property": {
"version": "1.0.2",
"from": "is-property@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
},
"is-resolvable": {
"version": "1.0.0",
"from": "is-resolvable@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz"
},
"isarray": {
"version": "1.0.0",
"from": "isarray@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
},
"js-yaml": {
"version": "3.6.1",
"from": "js-yaml@>=3.5.1 <4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz"
},
"json-stable-stringify": {
"version": "1.0.1",
"from": "json-stable-stringify@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz"
},
"jsonify": {
"version": "0.0.0",
"from": "jsonify@>=0.0.0 <0.1.0",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz"
},
"jsonpointer": {
"version": "4.0.0",
"from": "jsonpointer@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.0.tgz"
},
"levn": {
"version": "0.3.0",
"from": "levn@>=0.3.0 <0.4.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz"
},
"lodash": {
"version": "4.16.6",
"from": "lodash@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
},
"minimatch": {
"version": "3.0.3",
"from": "minimatch@>=3.0.2 <4.0.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"
},
"minimist": {
"version": "0.0.8",
"from": "minimist@0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
},
"mkdirp": {
"version": "0.5.1",
"from": "mkdirp@>=0.5.0 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
},
"ms": {
"version": "0.7.2",
"from": "ms@0.7.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz"
},
"mute-stream": {
"version": "0.0.5",
"from": "mute-stream@0.0.5",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz"
},
"natural-compare": {
"version": "1.4.0",
"from": "natural-compare@>=1.4.0 <2.0.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
},
"number-is-nan": {
"version": "1.0.1",
"from": "number-is-nan@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz"
},
"object-assign": {
"version": "4.1.0",
"from": "object-assign@>=4.0.1 <5.0.0",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz"
},
"once": {
"version": "1.4.0",
"from": "once@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
},
"onetime": {
"version": "1.1.0",
"from": "onetime@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz"
},
"optionator": {
"version": "0.8.2",
"from": "optionator@>=0.8.2 <0.9.0",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz"
},
"os-homedir": {
"version": "1.0.2",
"from": "os-homedir@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz"
},
"path-is-absolute": {
"version": "1.0.1",
"from": "path-is-absolute@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
},
"path-is-inside": {
"version": "1.0.2",
"from": "path-is-inside@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz"
},
"pify": {
"version": "2.3.0",
"from": "pify@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
},
"pinkie": {
"version": "2.0.4",
"from": "pinkie@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
},
"pinkie-promise": {
"version": "2.0.1",
"from": "pinkie-promise@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz"
},
"pluralize": {
"version": "1.2.1",
"from": "pluralize@>=1.2.1 <2.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz"
},
"prelude-ls": {
"version": "1.1.2",
"from": "prelude-ls@>=1.1.2 <1.2.0",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz"
},
"process-nextick-args": {
"version": "1.0.7",
"from": "process-nextick-args@>=1.0.6 <1.1.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
},
"progress": {
"version": "1.1.8",
"from": "progress@>=1.1.8 <2.0.0",
"resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz"
},
"readable-stream": {
"version": "2.0.6",
"from": "readable-stream@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz"
},
"readline2": {
"version": "1.0.1",
"from": "readline2@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz"
},
"require-uncached": {
"version": "1.0.3",
"from": "require-uncached@>=1.0.2 <2.0.0",
"resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz"
},
"resolve-from": {
"version": "1.0.1",
"from": "resolve-from@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz"
},
"restore-cursor": {
"version": "1.0.1",
"from": "restore-cursor@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz"
},
"rimraf": {
"version": "2.5.4",
"from": "rimraf@>=2.2.8 <3.0.0",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz"
},
"run-async": {
"version": "0.1.0",
"from": "run-async@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz"
},
"rx-lite": {
"version": "3.1.2",
"from": "rx-lite@>=3.1.2 <4.0.0",
"resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz"
},
"sax": {
"version": "1.2.1",
"from": "sax@>=1.1.4 <2.0.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz"
},
"shelljs": {
"version": "0.6.1",
"from": "shelljs@>=0.6.0 <0.7.0",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz"
},
"slice-ansi": {
"version": "0.0.4",
"from": "slice-ansi@0.0.4",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz"
},
"sprintf-js": {
"version": "1.0.3",
"from": "sprintf-js@>=1.0.2 <1.1.0",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
},
"string_decoder": {
"version": "0.10.31",
"from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"string-width": {
"version": "1.0.2",
"from": "string-width@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"
},
"strip-ansi": {
"version": "3.0.1",
"from": "strip-ansi@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
},
"strip-bom": {
"version": "3.0.0",
"from": "strip-bom@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
},
"strip-json-comments": {
"version": "1.0.4",
"from": "strip-json-comments@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"
},
"supports-color": {
"version": "2.0.0",
"from": "supports-color@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
},
"table": {
"version": "3.8.3",
"from": "table@>=3.7.8 <4.0.0",
"resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz",
"dependencies": {
"is-fullwidth-code-point": {
"version": "2.0.0",
"from": "is-fullwidth-code-point@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz"
},
"string-width": {
"version": "2.0.0",
"from": "string-width@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz"
}
}
},
"text-table": {
"version": "0.2.0",
"from": "text-table@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
},
"through": {
"version": "2.3.8",
"from": "through@>=2.3.6 <3.0.0",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
},
"tryit": {
"version": "1.0.3",
"from": "tryit@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz"
},
"type-check": {
"version": "0.3.2",
"from": "type-check@>=0.3.2 <0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz"
},
"typedarray": {
"version": "0.0.6",
"from": "typedarray@>=0.0.5 <0.1.0",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
},
"user-home": {
"version": "2.0.0",
"from": "user-home@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz"
},
"util-deprecate": {
"version": "1.0.2",
"from": "util-deprecate@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
},
"wordwrap": {
"version": "1.0.0",
"from": "wordwrap@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
},
"wrappy": {
"version": "1.0.2",
"from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
},
"write": {
"version": "0.2.1",
"from": "write@>=0.2.1 <0.3.0",
"resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz"
},
"xtend": {
"version": "4.0.1",
"from": "xtend@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
}
}
}

View file

@ -0,0 +1,16 @@
{
"name": "mach-eslint",
"description": "ESLint and external plugins for use with mach",
"repository": {},
"license": "MPL-2.0",
"dependencies": {
"eslint": "3.8.1",
"eslint-plugin-html": "1.5.2",
"eslint-plugin-react": "4.2.3",
"escope": "^3.6.0",
"espree": "^3.2.0",
"estraverse": "^4.2.0",
"ini-parser": "^0.0.2",
"sax": "^1.1.4"
}
}

70
tools/lint/eslint/update Normal file
View file

@ -0,0 +1,70 @@
#!/bin/sh
# Force the scripts working directory to be projdir/tools/lint/eslint.
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd $DIR
echo "To complete this script you will need the following tokens from https://api.pub.build.mozilla.org/tokenauth/"
echo " - tooltool.upload.public"
echo " - tooltool.download.public"
echo ""
read -p "Are these tokens visible at the above URL (y/n)?" choice
case "$choice" in
y|Y )
echo ""
echo "1. Go to https://api.pub.build.mozilla.org/"
echo "2. Log in using your Mozilla LDAP account."
echo "3. Click on \"Tokens.\""
echo "4. Issue a user token with the permissions tooltool.upload.public and tooltool.download.public."
echo ""
echo "When you click issue you will be presented with a long string. Paste the string into a temporary file called ~/.tooltool-token."
echo ""
read -rsp $'Press any key to continue...\n' -n 1
;;
n|N )
echo ""
echo "You will need to contact somebody that has these permissions... people most likely to have these permissions are members of the releng, ateam, a sheriff, mratcliffe, or jryans"
exit 1
;;
* )
echo ""
echo "Invalid input."
continue
;;
esac
echo ""
echo "Removing node_modules and npm_shrinkwrap.json..."
rm -rf node_modules/
rm npm-shrinkwrap.json
echo "Installing eslint and external plugins..."
# ESLint and all _external_ plugins are listed in this directory's package.json,
# so a regular `npm install` will install them at the specified versions.
# The in-tree eslint-plugin-mozilla is kept out of this tooltool archive on
# purpose so that it can be changed by any developer without requiring tooltool
# access to make changes.
npm install
echo "Creating npm shrinkwrap..."
npm shrinkwrap
echo "Creating eslint.tar.gz..."
tar cvfz eslint.tar.gz node_modules
echo "Downloading tooltool..."
wget https://raw.githubusercontent.com/mozilla/build-tooltool/master/tooltool.py
chmod +x tooltool.py
echo "Adding eslint.tar.gz to tooltool..."
rm manifest.tt
./tooltool.py add --visibility public eslint.tar.gz
echo "Uploading eslint.tar.gz to tooltool..."
./tooltool.py upload --authentication-file=~/.tooltool-token --message "node_modules folder update for tools/lint/eslint"
echo "Cleaning up..."
rm eslint.tar.gz
rm tooltool.py
echo ""
echo "Update complete, please commit and check in your changes."

195
tools/lint/flake8.lint Normal file
View file

@ -0,0 +1,195 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import os
import signal
import subprocess
import which
from mozprocess import ProcessHandler
from mozlint import result
here = os.path.abspath(os.path.dirname(__file__))
FLAKE8_REQUIREMENTS_PATH = os.path.join(here, 'flake8', 'flake8_requirements.txt')
FLAKE8_NOT_FOUND = """
Could not find flake8! Install flake8 and try again.
$ pip install -U --require-hashes -r {}
""".strip().format(FLAKE8_REQUIREMENTS_PATH)
FLAKE8_INSTALL_ERROR = """
Unable to install correct version of flake8
Try to install it manually with:
$ pip install -U --require-hashes -r {}
""".strip().format(FLAKE8_REQUIREMENTS_PATH)
LINE_OFFSETS = {
# continuation line under-indented for hanging indent
'E121': (-1, 2),
# continuation line missing indentation or outdented
'E122': (-1, 2),
# continuation line over-indented for hanging indent
'E126': (-1, 2),
# continuation line over-indented for visual indent
'E127': (-1, 2),
# continuation line under-indented for visual indent
'E128': (-1, 2),
# continuation line unaligned for hanging indend
'E131': (-1, 2),
# expected 1 blank line, found 0
'E301': (-1, 2),
# expected 2 blank lines, found 1
'E302': (-2, 3),
}
"""Maps a flake8 error to a lineoffset tuple.
The offset is of the form (lineno_offset, num_lines) and is passed
to the lineoffset property of `ResultContainer`.
"""
EXTENSIONS = ['.py', '.lint']
results = []
def process_line(line):
# Escape slashes otherwise JSON conversion will not work
line = line.replace('\\', '\\\\')
try:
res = json.loads(line)
except ValueError:
print('Non JSON output from linter, will not be processed: {}'.format(line))
return
if 'code' in res:
if res['code'].startswith('W'):
res['level'] = 'warning'
if res['code'] in LINE_OFFSETS:
res['lineoffset'] = LINE_OFFSETS[res['code']]
results.append(result.from_linter(LINTER, **res))
def run_process(cmdargs):
# flake8 seems to handle SIGINT poorly. Handle it here instead
# so we can kill the process without a cryptic traceback.
orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
proc = ProcessHandler(cmdargs, env=os.environ,
processOutputLine=process_line)
proc.run()
signal.signal(signal.SIGINT, orig)
try:
proc.wait()
except KeyboardInterrupt:
proc.kill()
def get_flake8_binary():
"""
Returns the path of the first flake8 binary available
if not found returns None
"""
binary = os.environ.get('FLAKE8')
if binary:
return binary
try:
return which.which('flake8')
except which.WhichError:
return None
def _run_pip(*args):
"""
Helper function that runs pip with subprocess
"""
try:
subprocess.check_output(['pip'] + list(args),
stderr=subprocess.STDOUT)
return True
except subprocess.CalledProcessError as e:
print(e.output)
return False
def reinstall_flake8():
"""
Try to install flake8 at the target version, returns True on success
otherwise prints the otuput of the pip command and returns False
"""
if _run_pip('install', '-U',
'--require-hashes', '-r',
FLAKE8_REQUIREMENTS_PATH):
return True
return False
def lint(files, **lintargs):
if not reinstall_flake8():
print(FLAKE8_INSTALL_ERROR)
return 1
binary = get_flake8_binary()
cmdargs = [
binary,
'--format', '{"path":"%(path)s","lineno":%(row)s,'
'"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}',
]
# Run any paths with a .flake8 file in the directory separately so
# it gets picked up. This means only .flake8 files that live in
# directories that are explicitly included will be considered.
# See bug 1277851
no_config = []
for f in files:
if not os.path.isfile(os.path.join(f, '.flake8')):
no_config.append(f)
continue
run_process(cmdargs+[f])
# XXX For some reason passing in --exclude results in flake8 not using
# the local .flake8 file. So for now only pass in --exclude if there
# is no local config.
exclude = lintargs.get('exclude')
if exclude:
cmdargs += ['--exclude', ','.join(lintargs['exclude'])]
if no_config:
run_process(cmdargs+no_config)
return results
LINTER = {
'name': "flake8",
'description': "Python linter",
'include': [
'python/mozlint',
'taskcluster',
'testing/firefox-ui',
'testing/marionette/client',
'testing/marionette/harness',
'testing/marionette/puppeteer',
'testing/mozbase',
'testing/mochitest',
'testing/talos/',
'tools/lint',
],
'exclude': ["testing/mozbase/mozdevice/mozdevice/Zeroconf.py",
'testing/mochitest/pywebsocket'],
'extensions': EXTENSIONS,
'type': 'external',
'payload': lint,
}

View file

@ -0,0 +1,4 @@
flake8==2.5.4 --hash=sha256:fb5a67af4024622287a76abf6b7fe4fb3cfacf765a790976ce64f52c44c88e4a
mccabe==0.4.0 --hash=sha256:cbc2938f6c01061bc6d21d0c838c2489664755cb18676f0734d7617f4577d09e
pep8==1.7.0 --hash=sha256:4fc2e478addcf17016657dff30b2d8d611e8341fac19ccf2768802f6635d7b8a
pyflakes==1.2.3 --hash=sha256:e87bac26c62ea5b45067cc89e4a12f56e1483f1f2cda17e7c9b375b9fd2f40da

Some files were not shown because too many files have changed in this diff Show more