Share Pastebin
Guest
Public paste!

Untitled

By: a guest | Mar 10th, 2010 | Syntax: Perl | Size: 13.67 KB | Hits: 45 | Expires: Never
This paste has a previous version, view the difference. Copy text to clipboard
  1. #!/usr/bin/perl
  2. #
  3. # Please comment any substantive changes with your email address and the date,
  4. # and add a short unique suffix to the version string.
  5.  
  6. our $VERSION = '1.1.1';
  7.  
  8.  
  9. =head1 NAME
  10.  
  11. cppdeps - Perform static analysis of C++ code
  12.  
  13. =head1 SYNOPSIS
  14.  
  15. cppdeps [options] [file ...]
  16.  
  17. =head1 DESCRIPTION
  18.  
  19. Prints a table listing the interdependencies of all C++ source and header
  20. files found in the search directory. Dependency is determined by #include
  21. directives found in each file. You can specify a dependency without using
  22. #include, by using '#pragma depends "filename"' instead. Other preprocessor
  23. macros (e.g. #ifdef) are not honoured. Perl's @INC array is used when trying
  24. to locate named files.
  25.  
  26. If files are listed on the command line, then those files will be inspected,
  27. otherwise a directory (usually the current directory) will be searched
  28. recursively for files with names ending in standard C++ extensions.
  29.  
  30. The first stage lists each component (i.e. source/header pair) in alphanumeric
  31. order with its direct dependencies indented below. The second stage lists the
  32. "levels" and the components which are members of each one. Direct circular
  33. dependency is indicated by an asterisk ('*'). Indirect circular dependency is
  34. not supported and behaviour in this case is undefined. Finally, it prints a
  35. coupling metric computed from the amount of interdependency found. Smaller is
  36. better for this metric; anything < 1.5 is good, anything > 2.0 is getting bad.
  37.  
  38. The members of level 1 are those components which can be tested independently
  39. of any other component. The members of level 2 are those components which can
  40. be tested independently of any component except one or more in level 1. The
  41. members of level N are those components which can be tested independently of
  42. any component in levels > N, but may depend on components in levels < N.
  43.  
  44. =head1 OPTIONS
  45.  
  46. =over 8
  47.  
  48. =item B<-d> | B<--dir> I<directory>
  49.  
  50. Specifies the directory through which to search (recursively). In the absence
  51. of this option, the default is the current working directory.
  52.  
  53. =item B<-h> | B<--help>
  54.  
  55. Prints this manual page (synopsis and options only) and exits.
  56.  
  57. =item B<-H> | B<--morehelp>
  58.  
  59. Prints this manual page (including detailed description) and exits.
  60.  
  61. =item B<-I> | B<--include> I<directory>
  62.  
  63. Adds a directory name to Perl's @INC array, which is used when searching for
  64. source files included from other source files.
  65.  
  66. =item B<-p> | B<--png> I<pngfile>
  67.  
  68. In addition to printing a textual analysis to stdout, uses GraphViz to create
  69. a PNG image file with the specified name, showing the interdependencies
  70. diagramatically.
  71.  
  72. =item B<-x> | B<--imageopt> I<option>
  73.  
  74. Specifies an option that will influence the generation of the PNG diagram:
  75.  
  76. =over 8
  77.  
  78. =item B<level>
  79.  
  80. Causes components to be arranged according to their levels.
  81.  
  82. =item B<dir>
  83.  
  84. Causes components to be grouped according to their parent directory.
  85.  
  86. =back
  87.  
  88. =item B<-r> | B<--regex> I<pattern>
  89.  
  90. Specifies a Perl regular expression against which to match filenames while
  91. searching. The default is '\.(h|H|hh|HH|hpp|hxx|c|C|cc|CC|cpp|cxx)$'. This
  92. option is ignored if filenames are supplied on the command line.
  93.  
  94. Be sure to quote the pattern to avoid expansion by your shell.
  95.  
  96. =item B<-v> | B<--version>
  97.  
  98. Prints the version number and exits.
  99.  
  100. =item B<-w> | B<--wildcard> I<pattern>
  101.  
  102. Specifies a simple pattern string against which to match filenames while
  103. searching. Only the asterisk ('*') is recognised as a special character.
  104. This option is ignored in the presence of the B<--regex> option, or if
  105. filenames are supplied on the command line.
  106.  
  107. Be sure to quote the pattern to avoid expansion by your shell.
  108.  
  109. =back
  110.  
  111. =head1 TODO
  112.  
  113. - Add an option to list indirect as well as direct dependencies.
  114.  
  115. - Support indirect circular dependencies.
  116.  
  117. - Support more languages (e.g. Java, Ruby, C#).
  118.  
  119. =head1 BIBLIOGRAPHY
  120.  
  121. I<Large Scale C++ Software Design>, John Lakos, 1996,
  122. Addison-Wesley Publishing.
  123.  
  124. =head1 AUTHOR
  125.  
  126. The Oktalist E<lt>I<mat@oktalist.com>E<gt>
  127.  
  128. =head1 COPYING
  129.  
  130. This program is free software. You are free to modify and/or redistribute
  131. it under the same terms as Perl itself.
  132.  
  133. =cut
  134.  
  135.  
  136. use strict;
  137. use warnings;
  138.  
  139. use Getopt::Long;
  140. use Pod::Usage;
  141.  
  142. GetOptions(\my %opts, qw(
  143.         dir|d=s
  144.         help|h
  145.         morehelp|H
  146.         include|I=s@
  147.         png|p=s
  148.         imageopt|x=s@
  149.         regex|r=s
  150.         version|v
  151.         wildcard|w=s
  152. )) or pod2usage(-verbose => 1, -exitval => 1);
  153.  
  154. $opts{morehelp} and pod2usage(-verbose => 2, -exitval => 0);
  155. $opts{help} and pod2usage(-verbose => 1, -exitval => 0);
  156. $opts{version} and (print("$0 version $VERSION\n"), exit 0);
  157.  
  158. $opts{dir} ||= '.';
  159. unshift @INC, $opts{dir}, @{ $opts{include} || [] };
  160.  
  161. defined $opts{wildcard} and $opts{regex} ||= wildcard2regex($opts{wildcard});
  162. $opts{regex} ||= '\.(h|H|hh|HH|hpp|hxx|c|C|cc|CC|cpp|cxx)$';
  163.  
  164. if (@ARGV)
  165. {
  166.         CppDeps::processFile($_, $opts{dir}, qr"$opts{regex}") foreach @ARGV;
  167. }
  168. else
  169. {
  170.         recurseDir($opts{dir}, \&CppDeps::processFile, qr"$opts{regex}");
  171. }
  172.  
  173. CppDeps::printDeps();
  174.  
  175. my @imageopts = split ',', join(',', @{ $opts{imageopt} || [] });
  176. $opts{png} and CppDeps::writePng($opts{png}, @imageopts);
  177.  
  178.  
  179. # sub recurseDir($dirname, $callbackfunc)
  180. #
  181. # Calls the coderef $callbackfunc->($filename) for each file $filename in the
  182. # directory $dirname, and calls recurseDir($subdirname, $callbackfunc) for each
  183. # subdirectory in the directory $dirname.
  184. #
  185. sub recurseDir
  186. {
  187.         my $dirname = shift;
  188.         my $callbackfunc = shift;
  189.  
  190.         if (opendir my $dir, $dirname)
  191.         {
  192.                 while (defined(my $direlemname = readdir $dir))
  193.                 {
  194.                         next if $direlemname =~ m'^\.';
  195.  
  196.                         if (-d "$dirname/$direlemname")
  197.                         {
  198.                                 recurseDir("$dirname/$direlemname", $callbackfunc, @_);
  199.                         }
  200.                         else
  201.                         {
  202.                                 $callbackfunc->($direlemname, $dirname, @_);
  203.                         }
  204.                 }
  205.         }
  206.         else
  207.         {
  208.                 warn "Can't open directory '$dirname'";
  209.         }
  210. }
  211.  
  212. # sub wildcard2regex
  213. #
  214. # Converts a simple asterisk wildcard into a regular expression string.
  215. #
  216. sub wildcard2regex
  217. {
  218.         my $wildcard = quotemeta shift;
  219.  
  220.         $wildcard =~ s{\\\*}{.*};
  221.         $wildcard = "^$wildcard\$";
  222.  
  223.         return $wildcard
  224. }
  225.  
  226.  
  227. package CppDeps;
  228.  
  229. use strict;
  230. use warnings;
  231.  
  232. our %FileDeps;
  233. # The component dependency bit-matrix. A true value in $FileDeps{A}->{B}
  234. # indicates that component A depends on component B.
  235.  
  236. our %FileLevels;
  237. # Hash associating components with levels. The numeric value $FileLevels{A}
  238. # denotes the level of component A.
  239.  
  240. our @Levels;
  241. # A list of lists containing the components contained within each level.
  242. # $Levels[N] is an arrayref containing the component names in level N.
  243.  
  244. our %FilesChecked;
  245. # Hash for remembering which files have been parsed so we don't parse any file
  246. # twice. A true value for $FilesChecked{A} indicates file A has been parsed.
  247.  
  248. our %CircularDepWarned;
  249. # A hash for keeping track of which circular dependencies have been warned
  250. # about, so we don't warn about the same one twice.
  251.  
  252. # sub CppDeps::processFile($filename [ , $regex [ , $currentDir ] ])
  253. #
  254. # Searches @INC for a file named $filename and calls findDeps($fullPathToFile)
  255. # on the first one found. Does nothing if $filename does not end in a standard
  256. # C or C++ file extension. $regex is an optional argument; a file will be
  257. # ignored if its name does not match. $currentDir is an optional argument; it
  258. # will be searched before @INC. Returns the full path of the file it found,
  259. # excluding the file extention. If no file was found, returns the empty string.
  260. #
  261. sub processFile
  262. {
  263.         my $filename = shift;
  264.         my $currentDir = shift;
  265.         my $regex = shift;
  266.  
  267.         if (defined $regex) { return '' unless $filename =~ $regex; }
  268.  
  269.         foreach my $incdir ($currentDir, @INC)
  270.         {
  271.                 next unless defined $incdir;
  272.  
  273.                 my $fqfilename = "$incdir/$filename";
  274.                 if (-f $fqfilename)
  275.                 {
  276.                         if (findDeps($fqfilename)) {
  277.                                 return extractBasename($fqfilename);
  278.                         }
  279.                 }
  280.         }
  281.         return '';
  282. }
  283.  
  284. # sub CppDeps::findDeps($filename)
  285. #
  286. # Reads the file $filename, and for each "#include" or "#pragma depends"
  287. # directive found, sets the dependency flag within the %FileDeps matrix and
  288. # calls processFile($includedFilename, $currentDir). Does nothing if $filename
  289. # has already been read.
  290. #
  291. sub findDeps
  292. {
  293.         my $filename = shift;
  294.  
  295.         $filename = normalizePath($filename);
  296.         my $basename = extractBasename($filename);
  297.         my $pathname = extractPathname($basename);
  298.  
  299.         if (exists $FilesChecked{$filename})
  300.         {
  301.                 return 1;
  302.         }
  303.         $FilesChecked{$filename} = 1;
  304.  
  305.         if (not exists $FileDeps{$basename})
  306.         {
  307.                 $FileDeps{$basename} = {};
  308.         }
  309.  
  310.         open my $fh, "< $filename" or return undef;
  311.  
  312.         while (defined(my $line = <$fh>))
  313.         {
  314.                 chomp $line;
  315.                 if ($line =~ m'^\s*#\s*(include|pragma\s+depends)\s+["<](.*?)[">]')
  316.                 {
  317.                         my $incfilename = $2;
  318.  
  319.                         $incfilename = processFile($incfilename, $pathname);
  320.  
  321.                         if ($incfilename)
  322.                         {
  323.                                 $incfilename = normalizePath($incfilename);
  324.                                 my $incbasename = extractBasename($incfilename);
  325.  
  326.                                 if ($basename ne $incbasename)
  327.                                 {
  328.                                         $FileDeps{$basename}->{$incbasename} = 1;
  329.                                 }
  330.                         }
  331.                 }
  332.         } return 1;
  333. }
  334.  
  335. # sub CppDeps::printDeps()
  336. #
  337. # Pretty-prints the %FileDeps matrix, then populates the %FileLevels hash and
  338. # the @Levels array and then pretty-prints that too. Finally, computes and
  339. # prints a coupling metric based on the amount of interdependence found.
  340. #
  341. sub printDeps
  342. {
  343.         %FileDeps or (print("No files found. Use the -h option for help.\n"), return);
  344.  
  345.         foreach my $key (sort keys %FileDeps)
  346.         {
  347.                 my @deps = sort keys %{ $FileDeps{$key} };
  348.  
  349.                 print "$key:\n";
  350.                 print "\t$_\n" foreach @deps;
  351.                 print "\n";
  352.  
  353.                 if (0 == @deps)
  354.                 {
  355.                         $FileLevels{$key} = 1;
  356.                 }
  357.         }
  358.  
  359.         for (0..keys %FileDeps)
  360.         {
  361.                 foreach my $key (keys %FileDeps)
  362.                 {
  363.                         my $basename = extractBasename($key);
  364.  
  365.                         my $level = exists($FileLevels{$key})
  366.                                     ? $FileLevels{$key}
  367.                                     : 0;
  368.  
  369.                         foreach my $dep (keys %{ $FileDeps{$key} })
  370.                         {
  371.                                 my $depbasename = extractBasename($dep);
  372.  
  373.                                 my $deplevel = exists($FileLevels{$dep})
  374.                                                  ? $FileLevels{$dep}
  375.                                                  : -1;
  376.  
  377.                                 if ($level < $deplevel + 1
  378.                                     and $basename ne $depbasename)
  379.                                 {
  380.                                         if (exists $FileDeps{$dep}->{$key})
  381.                                         {
  382.                                                 $FileLevels{$key} = $deplevel;
  383.  
  384.                                                 warnCircularDep($key, $dep);
  385.                                         }
  386.                                         else
  387.                                         {
  388.                                                 $FileLevels{$key} = $deplevel + 1;
  389.                                         }
  390.                                 }
  391.                         }
  392.                 }
  393.         }
  394.  
  395.         foreach my $key (keys %FileLevels)
  396.         {
  397.                 push @{ $Levels[ $FileLevels{$key} ] }, $key;
  398.         }
  399.  
  400.         foreach my $level (0..@Levels)
  401.         {
  402.                 next unless defined $Levels[$level];
  403.  
  404.                 # follow indirect dependencies
  405.                 foreach my $key (@{ $Levels[$level] })
  406.                 {
  407.                         foreach my $dep (keys %{ $FileDeps{$key} })
  408.                         {
  409.                                 $FileDeps{$key} = {
  410.                                         %{ $FileDeps{$key} },
  411.                                         %{ $FileDeps{$dep} }
  412.                                 };
  413.                         }
  414.                 }
  415.  
  416.                 my @deps = sort @{ $Levels[$level] };
  417.  
  418.                 print "LEVEL $level:\n";
  419.  
  420.                 foreach my $dep (@deps)
  421.                 {
  422.                         if (exists $CircularDepWarned{$dep})
  423.                         {
  424.                                 print "\t$dep *\n";
  425.                         }
  426.                         else
  427.                         {
  428.                                 print "\t$dep\n";
  429.                         }
  430.                 }
  431.  
  432.                 print "\n" if @deps;
  433.         }
  434.  
  435.         my $filecount = 0;
  436.         my $depcount = 0;
  437.         foreach my $key (keys %FileDeps)
  438.         {
  439.                 $filecount++;
  440.                 $depcount += keys %{ $FileDeps{$key} };
  441.         }
  442.         my $nccd = $depcount / ($filecount * log $filecount);
  443.         printf "COUPLING_METRIC = %.4f\n", $nccd;
  444. }
  445.  
  446. # sub CppDeps::writePng($filename)
  447. #
  448. # Uses GraphViz to draw and save a PNG diagram of the dependency graph.
  449. #
  450. sub writePng
  451. {
  452.         my $filename = shift;
  453.         my %opts;
  454.         $opts{$_} = 1 foreach @_;
  455.  
  456.         require GraphViz;
  457.  
  458.         my $graph = new GraphViz (
  459.                 directed => 1,
  460.                 bgcolor => 'white',
  461.                 name => 'cppdeps',
  462.                 concentrate => 1,
  463.                 rankdir => 1,
  464.                 node => {
  465.                         shape => 'box'
  466.                 },
  467.                 edge => {
  468.                         style => 'bold',
  469.                         arrowsize => 2
  470.                 }
  471.         );
  472.  
  473.         foreach my $level (0..(@Levels - 1))
  474.         {
  475.                 foreach my $key (@{ $Levels[$level] })
  476.                 {
  477.                         my %nodeopts;
  478.                         $opts{level} and $nodeopts{rank} = $level;
  479.                         $opts{dir} and $nodeopts{cluster} = extractPathname($key);
  480.                         $opts{dir} and $nodeopts{label} = extractFilename($key);
  481.  
  482.                         $graph->add_node($key, %nodeopts);
  483.                 }
  484.         }
  485.  
  486.         foreach my $key (keys %FileDeps)
  487.         {
  488.                 foreach my $dep (keys %{ $FileDeps{$key} })
  489.                 {
  490.                         my $draw = 1;
  491.                         foreach my $dep2 (keys %{ $FileDeps{$key} })
  492.                         {
  493.                 # remove direct dependency wherever an indirect dependency exists
  494.                                 if (exists $FileDeps{$dep2}{$dep}) { $draw = 0; last }
  495.                         }
  496.  
  497.                         $draw and $graph->add_edge($key => $dep);
  498.                 }
  499.         }
  500.  
  501.         $graph->as_png($filename);
  502. }
  503.  
  504. # sub CppDeps::warnCircularDep($filename1, $filename2)
  505. #
  506. # Sets a bit in the %CircularDepWarned matrix, identifying that a circular
  507. # dependency exists between $filename1 and $filename2.
  508. #
  509. sub warnCircularDep
  510. {
  511.         my $file1 = shift;
  512.         my $file2 = shift;
  513.  
  514.         $CircularDepWarned{$file1}->{$file2} = 1;
  515.         $CircularDepWarned{$file2}->{$file1} = 1;
  516. }
  517.  
  518. # sub CppDeps::normalizePath($path)
  519. #
  520. # Returns $path with Windows filesystem "\" replaced with Unix style "/" and any
  521. # number of leading "./" substrings stripped from the front.
  522. #
  523. sub normalizePath
  524. {
  525.         my $path = shift;
  526.  
  527.         $path =~ s{\\}{/}g;
  528.         $path =~ s{\G\./}{}g;
  529.  
  530.         return $path;
  531. }
  532.  
  533. # sub CppDeps::extractBasename($filename)
  534. #
  535. # Returns $filename with any file extension removed from the end.
  536. #
  537. sub extractBasename
  538. {
  539.         my $file = shift;
  540.  
  541.         $file =~ s{\.[^\./]+$}{};
  542.  
  543.         return $file;
  544. }
  545.  
  546. # sub CppDeps::extractPathname($filename)
  547. #
  548. # Returns $filename with everything beyond the final "/" removed, leaving only
  549. # the directory in which $filename is located.
  550. #
  551. sub extractPathname
  552. {
  553.         my $file = shift;
  554.  
  555.         $file =~ s{/[^/*]+$}{};
  556.  
  557.         return $file;
  558. }
  559.  
  560. # sub CppDeps::extractFilename($filename)
  561. #
  562. # Returns $filename with everything before the final "/" removed, leaving only
  563. # the name of the file relative to the directory in which it is located.
  564. #
  565. sub extractFilename
  566. {
  567.         my $file = shift;
  568.  
  569.         $file =~ s{^.*/}{};
  570.  
  571.         return $file;
  572. }
  573.  
  574. 1;