Create a tool to catch #include omissions that might not result in any

compiler warning, specifically #ifdef or #if defined tests on symbols
that are defined in a file not included.  The results are a bit noisy
and require care to interpret, but it's a lot better than no tool at all.
This commit is contained in:
Tom Lane 2006-07-15 03:27:42 +00:00
parent 98bac16e4d
commit 2d11d26113
2 changed files with 246 additions and 0 deletions

View File

@ -10,6 +10,11 @@ pgcompinclude [-v]
pgrminclude [-v] pgrminclude [-v]
remove extra #include's remove extra #include's
pgcheckdefines
check for #ifdef tests on symbols defined in files that
weren't included --- this is a necessary sanity check on
pgrminclude!
pgdefine create macro calls for all defines in the file (used by pgdefine create macro calls for all defines in the file (used by
the above routines) the above routines)
@ -22,3 +27,4 @@ order would be:
pgrminclude /src/include pgrminclude /src/include
pgcompinclude pgcompinclude
pgrminclude / pgrminclude /
pgcheckdefines

View File

@ -0,0 +1,240 @@
#! /usr/bin/perl -w
#
# This script looks for symbols that are referenced in #ifdef or defined()
# tests without having #include'd the file that defines them. Since this
# situation won't necessarily lead to any compiler message, it seems worth
# having an automated check for it. In particular, use this to audit the
# results of pgrminclude!
#
# Usage: configure and build a PG source tree (non-VPATH), then start this
# script at the top level. It's best to enable as many configure options
# as you can, especially --enable-cassert which is known to affect include
# requirements. NB: you MUST use gcc, unless you have another compiler that
# can be persuaded to spit out the names of referenced include files.
#
# The results are necessarily platform-dependent, so use care in interpreting
# them. We try to process all .c files, even those not intended for the
# current platform, so there will be some phony failures.
#
# $PostgreSQL: pgsql/src/tools/pginclude/pgcheckdefines,v 1.1 2006/07/15 03:27:42 tgl Exp $
#
use Cwd;
use File::Basename;
$topdir = cwd();
# Programs to use
$FIND = "find";
$MAKE = "make";
#
# Build arrays of all the .c and .h files in the tree
#
# We ignore .h files under src/include/port/, since only the one exposed as
# src/include/port.h is interesting. (XXX Windows ports have additional
# files there?) Ditto for .h files in src/backend/port/ subdirectories.
# Including these .h files would clutter the list of define'd symbols and
# cause a lot of false-positive results.
#
open PIPE, "$FIND * -type f -name '*.c' |"
or die "can't fork: $!";
while (<PIPE>) {
chomp;
push @cfiles, $_;
}
close PIPE or die "$FIND failed: $!";
open PIPE, "$FIND * -type f -name '*.h' |"
or die "can't fork: $!";
while (<PIPE>) {
chomp;
push @hfiles, $_ unless
m|^src/include/port/| ||
m|^src/backend/port/\w+/|;
}
close PIPE or die "$FIND failed: $!";
#
# For each .h file, extract all the symbols it #define's, and add them to
# a hash table. To cover the possibility of multiple .h files defining
# the same symbol, we make each hash entry a hash of filenames.
#
foreach $hfile (@hfiles) {
open HFILE, $hfile
or die "can't open $hfile: $!";
while (<HFILE>) {
if (m/^\s*#\s*define\s+(\w+)/) {
$defines{$1}{$hfile} = 1;
}
}
close HFILE;
}
#
# For each file (both .h and .c), run the compiler to get a list of what
# files it #include's. Then extract all the symbols it tests for defined-ness,
# and check each one against the previously built hashtable.
#
foreach $file (@hfiles, @cfiles) {
($fname, $fpath) = fileparse($file);
chdir $fpath or die "can't chdir to $fpath: $!";
#
# Ask 'make' to parse the makefile so we can get the correct flags to
# use. CPPFLAGS in particular varies for each subdirectory. If we are
# processing a .h file, we might be in a subdirectory that has no
# Makefile, in which case we have to fake it. Note that there seems
# no easy way to prevent make from recursing into subdirectories and
# hence printing multiple definitions --- we keep the last one, which
# should come from the current Makefile.
#
if (-f "Makefile" || -f "GNUmakefile") {
$MAKECMD = "$MAKE -qp";
} else {
$subdir = $fpath;
chop $subdir;
$top_builddir = "..";
$tmp = $fpath;
while (($tmp = dirname($tmp)) ne '.') {
$top_builddir = $top_builddir . "/..";
}
$MAKECMD = "$MAKE -qp 'subdir=$subdir' 'top_builddir=$top_builddir' -f '$top_builddir/src/Makefile.global'";
}
open PIPE, "$MAKECMD |"
or die "can't fork: $!";
while (<PIPE>) {
if (m/^CPPFLAGS :?= (.*)/) {
$CPPFLAGS = $1;
} elsif (m/^CFLAGS :?= (.*)/) {
$CFLAGS = $1;
} elsif (m/^CFLAGS_SL :?= (.*)/) {
$CFLAGS_SL = $1;
} elsif (m/^PTHREAD_CFLAGS :?= (.*)/) {
$PTHREAD_CFLAGS = $1;
} elsif (m/^CC :?= (.*)/) {
$CC = $1;
}
}
# If make exits with status 1, it's not an error, it just means make
# thinks some files may not be up-to-date. Only complain on status 2.
close PIPE;
die "$MAKE failed in $fpath\n" if $? != 0 && $? != 256;
# Expand out stuff that might be referenced in CFLAGS
$CFLAGS =~ s/\$\(CFLAGS_SL\)/$CFLAGS_SL/;
$CFLAGS =~ s/\$\(PTHREAD_CFLAGS\)/$PTHREAD_CFLAGS/;
#
# Run the compiler (which had better be gcc) to get the inclusions.
# "gcc -H" reports inclusions on stderr as "... filename" where the
# number of dots varies according to nesting depth.
#
@includes = ();
$COMPILE = "$CC $CPPFLAGS $CFLAGS -H -E $fname";
open PIPE, "$COMPILE 2>&1 >/dev/null |"
or die "can't fork: $!";
while (<PIPE>) {
if (m/^\.+ (.*)/) {
$include = $1;
# Ignore system headers (absolute paths); but complain if a
# .c file includes a system header before any PG header.
if ($include =~ m|^/|) {
warn "$file includes $include before any Postgres inclusion\n"
if $#includes == -1 && $file =~ m/\.c$/;
next;
}
# Strip any "./" (assume this appears only at front)
$include =~ s|^\./||;
# Make path relative to top of tree
$ipath = $fpath;
while ($include =~ s|^\.\./||) {
$ipath = dirname($ipath) . "/";
}
$ipath =~ s|^\./||;
push @includes, $ipath . $include;
} else {
warn "$CC: $_";
}
}
# The compiler might fail, particularly if we are checking a file that's
# not supposed to be compiled at all on the current platform, so don't
# quit on nonzero status.
close PIPE or warn "$COMPILE failed in $fpath\n";
#
# Scan the file to find #ifdef, #ifndef, and #if defined() constructs
# We assume #ifdef isn't continued across lines, and that defined(foo)
# isn't split across lines either
#
open FILE, $fname
or die "can't open $file: $!";
$inif = 0;
while (<FILE>) {
$line = $_;
if ($line =~ m/^\s*#\s*ifdef\s+(\w+)/) {
$symbol = $1;
&checkit;
}
if ($line =~ m/^\s*#\s*ifndef\s+(\w+)/) {
$symbol = $1;
&checkit;
}
if ($line =~ m/^\s*#\s*if\s+/) {
$inif = 1;
}
if ($inif) {
while ($line =~ s/\bdefined(\s+|\s*\(\s*)(\w+)//) {
$symbol = $2;
&checkit;
}
if (!($line =~ m/\\$/)) {
$inif = 0;
}
}
}
close FILE;
chdir $topdir or die "can't chdir to $topdir: $!";
}
exit 0;
# Check an is-defined reference
sub checkit {
# Ignore if symbol isn't defined in any PG include files
if (! defined $defines{$symbol}) {
return;
}
#
# Try to match source(s) of symbol to the inclusions of the current file
# (including itself). We consider it OK if any one matches.
#
# Note: these tests aren't bulletproof; in theory the inclusion might
# occur after the use of the symbol. Given our normal file layout,
# however, the risk is minimal.
#
foreach $deffile (keys %{ $defines{$symbol} }) {
return if $deffile eq $file;
foreach $reffile (@includes) {
return if $deffile eq $reffile;
}
}
#
# If current file is a .h file, it's OK for it to assume that one of the
# base headers (postgres.h or postgres_fe.h) has been included.
#
if ($file =~ m/\.h$/) {
foreach $deffile (keys %{ $defines{$symbol} }) {
return if $deffile eq 'src/include/c.h';
return if $deffile eq 'src/include/postgres.h';
return if $deffile eq 'src/include/postgres_fe.h';
return if $deffile eq 'src/include/pg_config.h';
return if $deffile eq 'src/include/pg_config_manual.h';
}
}
#
@places = keys %{ $defines{$symbol} };
print "$file references $symbol, defined in @places\n";
# print "includes: @includes\n";
}