Create a tool to catch #include omissions that might not result in any
authorTom Lane
Sat, 15 Jul 2006 03:27:42 +0000 (03:27 +0000)
committerTom Lane
Sat, 15 Jul 2006 03:27:42 +0000 (03:27 +0000)
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.

src/tools/pginclude/README
src/tools/pginclude/pgcheckdefines [new file with mode: 0755]

index 0100562cfed647f0370f17136c8bcf0d882bf8a9..ac6cd7edfe23dae99d2a0507e26572a5cf97292e 100644 (file)
@@ -10,6 +10,11 @@ pgcompinclude [-v]
 pgrminclude [-v]
        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
        the above routines)
 
@@ -22,3 +27,4 @@ order would be:
    pgrminclude /src/include
    pgcompinclude
    pgrminclude /
+   pgcheckdefines
diff --git a/src/tools/pginclude/pgcheckdefines b/src/tools/pginclude/pgcheckdefines
new file mode 100755 (executable)
index 0000000..07ae7b5
--- /dev/null
@@ -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 () {
+    chomp;
+    push @cfiles, $_;
+}
+close PIPE or die "$FIND failed: $!";
+
+open PIPE, "$FIND * -type f -name '*.h' |"
+    or die "can't fork: $!";
+while () {
+    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 () {
+   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 () {
+   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 () {
+   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 () {
+   $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";
+}