generalized design: grouping is a transformation
[deps.git] / graph-includes
blob70b4ec55ee9f926c2503a50ed7179de786029e8c
1 #!/usr/bin/env perl
3 # graph-includes - create a graphviz graph of source-files
4 # dependencies, with an emphasis on getting usable graphs even for
5 # large projects
7 # (c) 2005 Yann Dirson <ydirson@altern.org>
8 # Distributed under version 2 of the GNU GPL.
10 use warnings;
11 use strict;
13 use File::Basename qw(dirname);
14 use lib dirname($0).'/lib';
16 use Getopt::Long qw(GetOptions);
17 use List::Util qw(sum);
18 use File::Find qw(find);
19 use graphincludes::params;
21 #FIXME: also set arrow head
22 our $paperparams='-Gnodesep=0.1 -Granksep=0.1 -Nfontsize=5 -Efontsize=5';
23 our %paper = (
24 a4 => '11.6,8.2',
25 a3 => '16.5,11.6',
26 letter => '11,8.5',
29 our $showalldeps=0;
30 our $class='default';
31 our $language='C';
32 our (@colors);
33 our ($outfile, $prefixstrip, $paper);
35 our $usage = <<EOF;
36 Usage: $0 [options] src/*.[ch]
37 Options:
38 -class {default|uniqueincludes|<your-own-class>}
39 Select "class" of source code
40 -language <lang> Select language syntax for dependency extraction (default: C)
41 -fileregexp <perl-regexp>
42 Use this regexp to identify interesting files inside directories
43 (overrides per-language default regexp)
44 -Include <directory> Adds a directory to the path where to look for project's include files
45 -sysInclude <directory> Adds a directory to the path where to look for system include files
46 -prefixstrip <prefix> Strip <prefix> (eg. "src/") from filenames in the graph
47 -group <min>-<max> Draw file groups of levels <min> through <max> (default: 1 1)
48 -color <n>:<label>=<color>[,<label>=<color>...]
49 Use specified colors to show members of level-<n> group labelled <label>
50 -alldeps Do not apply transitive reduction to the graph
52 -showdropped Show in special color edges dropped during transitive reduction
53 -focus <node-label> Like -showdropped but only for edges starting from given node
55 -output <outfile>.<fmt>
56 Format to output file, using <fmt> as target format
57 -paper a4|a3|letter Select paper size of multi-paged postscript output
59 -verbose Show progress
60 -debug Loads of debuging output
62 -version Display this program's version
63 -help This help text
64 EOF
66 our @colspecs;
68 # memorize command-line for the report
69 our @commandline = @ARGV;
71 GetOptions ('alldeps' => \$showalldeps,
72 'showdropped' => \$graphincludes::params::showdropped,
74 'focus=s' => \@graphincludes::params::focus,
75 'class=s' => \$class,
76 'language=s' => \$language,
77 'fileregexp=s' => \$graphincludes::params::filename_regexp,
79 'Include=s' => \@graphincludes::params::inclpath,
80 'sysInclude=s' => \@graphincludes::params::sysinclpath,
82 'group=s' => sub {
83 my (undef, $range) = @_;
84 ($graphincludes::params::minshow, $graphincludes::params::maxshow) = split /-/, $range;
86 'color=s@' => sub {
87 my (undef, $colspec) = @_;
88 my @temp = split /:/, $colspec;
89 push @colspecs, [$temp[1], $temp[2]];
91 'output=s' => \$outfile,
92 'paper=s' => \$paper,
94 'prefixstrip=s' => \$prefixstrip,
96 'verbose+' => \$graphincludes::params::verbose,
97 'debug' => \$graphincludes::params::debug,
98 'help' => sub { print $usage; exit 0; },
99 'version' => sub { print "$0 version $graphincludes::params::VERSION\n"; exit 0; },
101 ) or print STDERR $usage and exit 1;
103 if (@ARGV == 0) {
104 print STDERR $usage;
105 exit 1;
108 # deal with non-default output formats
110 our $dotflags = '';
111 our $outputformat;
113 if (defined $paper) {
114 die "Unkown paper size \`$paper'" unless defined $paper{$paper};
115 # paper output format is postscript on stdout unless otherwise specified
116 $outputformat = 'ps';
117 $dotflags .= " $paperparams -Gpage=" . $paper{$paper};
120 if (defined $outfile) {
121 $dotflags .= " -o $outfile";
123 $outfile =~ m/.*\.([^.]+)$/ or die "cannot guess output format";
124 $outputformat = $1;
127 $dotflags .= " -T$outputformat" if defined $outputformat;
129 if ($dotflags ne '') {
130 print STDERR "Running through \`dot $dotflags'\n" if $graphincludes::params::verbose;
131 open STDOUT, "| dot $dotflags";
134 # create a project with specified files
135 our $classmodule = "graphincludes::project::" . $class;
136 eval "require $classmodule" or die "cannot load $classmodule from " . join ':', @INC;
137 $classmodule->set_language ($language) or die "cannot set language to '$language'";
138 our @files;
139 foreach my $arg (@ARGV) {
140 if (-d $arg) {
141 find ( { no_chdir => 0,
142 wanted => sub {
143 if ($classmodule->accepts_file ($_)) {
144 push @files, $File::Find::name;
145 print STDERR "Adding $File::Find::name\n" if $graphincludes::params::debug;
147 } }, $arg);
148 } elsif (-r $arg) {
149 push @files, $arg;
150 } else {
151 die "file does not exist: $arg";
154 our $project = ($classmodule)->new(prefixstrip => $prefixstrip,
155 files => \@files);
156 $project->init();
158 @colors = $project->defaultcolors();
159 foreach my $colspec (@colspecs) {
160 foreach my $coldef (split /,/, $colspec->[2]) {
161 my @coldef = split /=/, $coldef;
162 $colors[$colspec->[1]]->{$coldef[0]} = $coldef[1];
166 our $stat_nfiles = scalar @{$project->{FILES}};
167 # NOTE: $stat_nedges below is a cut'n'paste of $stat_ndeps
168 our $stat_ndeps = sum (map { scalar keys %{$project->{DEPS}{$_}} } (keys %{$project->{DEPS}}));
170 if (!defined $stat_ndeps) {
171 print STDERR "$0: found no dependency\n";
172 exit 0;
175 # maybe get rid of shortcut deps (transitive reduction)
176 $project->reduce() unless ($showalldeps);
178 our $stat_nnodes = scalar keys %{$project->{NODES}};
179 our $stat_nleaves = $stat_nnodes - scalar keys %{$project->{DEPS}};
180 # NOTE: $stat_ndeps above is a cut'n'paste of $stat_nedges
181 our $stat_nedges = sum (map { scalar keys %{$project->{DEPS}{$_}} } (keys %{$project->{DEPS}}));
183 # print graph
185 print "strict digraph dependencies {\nrankdir=LR;\n";
187 sub sprintnode {
188 my ($file, $min, $max) = @_;
189 my $node = $project->filelabel($file,$max);
190 if (!defined $node and $max > $min) {
191 return sprintnode($file, $min, $max-1);
192 } elsif ($min == $max) {
193 my $fill='';
194 $fill=",style=filled,fillcolor=" . $colors[2]->{$project->filelabel($file,2)}
195 if defined $colors[2] and defined $project->filelabel($file,2) and
196 defined $colors[2]->{$project->filelabel($file,2)};
197 return $project->{NODEIDS}->{$node} . " [label=\"$node\"" . $fill . "];";
198 } else {
199 return "subgraph \"cluster $node\" {" . sprintnode($file, $min, $max-1) . '}';
203 foreach my $file (@{$project->{FILES}}) {
204 print sprintnode($file, $graphincludes::params::minshow, $graphincludes::params::maxshow), "\n";
207 foreach my $file ($project->get_dep_origins) {
208 foreach my $dest ($project->get_dependencies($file)) {
209 print "$file -> $dest";
210 my $special = $project->special_edge($file, $dest);
211 # special handling for label, as array
212 push @{$special->{label}}, '[' . $project->get_dependency_weight($file, $dest) . ']';
213 $special->{label} = join '\n', @{$special->{label}};
215 print " [", join (',', map {$_ . '="' . $special->{$_} . '"'} keys %$special), "]" if defined $special;
216 print ";\n";
220 print "}\n";
223 # print report
225 our $report = 'graph-includes.report';
226 $report = $outfile . '.' . $report if defined $outfile;
227 open REPORT, ">$report" or die "cannot open $report for writing: $!";
228 print REPORT "\n Graph-includes report";
229 print REPORT "\n =====================\n";
231 print REPORT "\nGeneral statistics:";
232 print REPORT "\n-------------------\n\n";
233 print REPORT "$stat_nfiles files, $stat_nnodes nodes (",
234 int(100*($stat_nfiles-$stat_nnodes)/$stat_nfiles), "% dropped)\n";
235 print REPORT "$stat_ndeps dependencies, $stat_nedges edges (",
236 int(100*($stat_ndeps-$stat_nedges)/$stat_ndeps), "% dropped)\n";
237 print REPORT "$stat_nleaves leaf node(s)\n";
239 print REPORT "\n";
240 print REPORT scalar keys %{$project->{REPORT}->{HDR}}, " dependencies not found\n";
241 print REPORT scalar keys %{$project->{REPORT}->{SYS}}, " dependencies identified as system headers\n";
243 print REPORT "\nDeclared dependencies not found:";
244 print REPORT "\n--------------------------------\n\n";
245 for my $dep (sort keys %{$project->{REPORT}->{HDR}}) {
246 print REPORT " $dep\n";
247 for my $src (@{$project->{REPORT}->{HDR}->{$dep}}) {
248 print REPORT " from $src\n";
252 print REPORT "\nUsed system headers:";
253 print REPORT "\n--------------------\n\n";
254 for my $dep (sort keys %{$project->{REPORT}->{SYS}}) {
255 print REPORT " $dep\n";
258 print REPORT "\nCommand-line used:";
259 print REPORT "\n------------------\n\n";
260 # display arguments separated by space, quoting any argument with embedded whitespace
261 print REPORT "$0 ", join ' ', map { m/\s/ ? "\"$_\"" : $_ } @commandline;
263 print REPORT "\n\nThis was $0 version $graphincludes::params::VERSION\n";
264 print REPORT "\n=== End of report ===\n";
265 close REPORT;
267 # wait for dot if needed
268 if ($dotflags ne '') {
269 close STDOUT;
270 wait;