]> git.ipfire.org Git - thirdparty/openssl.git/blame - util/find-doc-nits
util/find-doc-nits: Fine tune detection of POD markup in NAME section
[thirdparty/openssl.git] / util / find-doc-nits
CommitLineData
1bc74519 1#! /usr/bin/env perl
95f92d57 2# Copyright 2002-2019 The OpenSSL Project Authors. All Rights Reserved.
05ea606a 3#
9059ab42 4# Licensed under the Apache License 2.0 (the "License"). You may not use
05ea606a
RS
5# this file except in compliance with the License. You can obtain a copy
6# in the file LICENSE in the source distribution or at
7# https://www.openssl.org/source/license.html
8
1bc74519
RS
9
10require 5.10.0;
11use warnings;
12use strict;
13use Pod::Checker;
14use File::Find;
169a8e39 15use File::Basename;
71a8b855 16use File::Spec::Functions;
35ea640a 17use Getopt::Std;
71a8b855
RS
18use lib catdir(dirname($0), "perl");
19use OpenSSL::Util::Pod;
35ea640a 20
71a8b855 21# Options.
8d50b9c1 22our($opt_d);
71a8b855 23our($opt_h);
9e183d22 24our($opt_l);
8d50b9c1 25our($opt_n);
274d1bee 26our($opt_p);
8d50b9c1 27our($opt_u);
e75138ab 28our($opt_c);
71a8b855
RS
29
30sub help()
31{
32 print <<EOF;
33Find small errors (nits) in documentation. Options:
8d50b9c1 34 -d Detailed list of undocumented (implies -u)
9e183d22 35 -l Print bogus links
71a8b855 36 -n Print nits in POD pages
274d1bee 37 -p Warn if non-public name documented (implies -n)
ee4afacd 38 -u Count undocumented functions
71a8b855 39 -h Print this help message
e75138ab 40 -c List undocumented commands and options
71a8b855
RS
41EOF
42 exit;
43}
1bc74519 44
05ea606a
RS
45my $temp = '/tmp/docnits.txt';
46my $OUT;
274d1bee 47my %public;
05ea606a 48
169a8e39
RL
49my %mandatory_sections =
50 ( '*' => [ 'NAME', 'DESCRIPTION', 'COPYRIGHT' ],
3dfda1a6
RS
51 1 => [ 'SYNOPSIS', 'OPTIONS' ],
52 3 => [ 'SYNOPSIS', 'RETURN VALUES' ],
169a8e39
RL
53 5 => [ ],
54 7 => [ ] );
169a8e39 55
35ea640a
RS
56# Cross-check functions in the NAME and SYNOPSIS section.
57sub name_synopsis()
58{
59 my $id = shift;
60 my $filename = shift;
61 my $contents = shift;
62
35ea640a
RS
63 # Get NAME section and all words in it.
64 return unless $contents =~ /=head1 NAME(.*)=head1 SYNOPSIS/ms;
65 my $tmp = $1;
66 $tmp =~ tr/\n/ /;
3ba4dac6 67 print "$id trailing comma before - in NAME\n" if $tmp =~ /, *-/;
2bcb232e 68 $tmp =~ s/ -.*//g;
1f79ddf5 69 print "$id POD markup among the names in NAME\n" if $tmp =~ /[<>]/;
2bcb232e
RS
70 $tmp =~ s/ */ /g;
71 print "$id missing comma in NAME\n" if $tmp =~ /[^,] /;
fbba5d11
RS
72
73 my $dirname = dirname($filename);
74 my $simplename = basename($filename);
75 $simplename =~ s/.pod$//;
76 my $foundfilename = 0;
77 my %foundfilenames = ();
35ea640a 78 my %names;
23ab880d
RL
79 foreach my $n ( split ',', $tmp ) {
80 $n =~ s/^\s+//;
81 $n =~ s/\s+$//;
82 print "$id the name '$n' contains white-space\n"
83 if $n =~ /\s/;
35ea640a 84 $names{$n} = 1;
fbba5d11
RS
85 $foundfilename++ if $n eq $simplename;
86 $foundfilenames{$n} = 1
87 if -f "$dirname/$n.pod" && $n ne $simplename;
35ea640a 88 }
fbba5d11
RS
89 print "$id the following exist as other .pod files:\n",
90 join(" ", sort keys %foundfilenames), "\n"
91 if %foundfilenames;
274d1bee 92 print "$id $simplename (filename) missing from NAME section\n"
fbba5d11 93 unless $foundfilename;
1722496f
RS
94 foreach my $n ( keys %names ) {
95 print "$id $n is not public\n"
96 if $opt_p and !defined $public{$n};
97 }
35ea640a
RS
98
99 # Find all functions in SYNOPSIS
100 return unless $contents =~ /=head1 SYNOPSIS(.*)=head1 DESCRIPTION/ms;
101 my $syn = $1;
102 foreach my $line ( split /\n+/, $syn ) {
be80b21d 103 next unless $line =~ /^\s/;
8162f6f5 104 my $sym;
c952780c 105 $line =~ s/STACK_OF\([^)]+\)/int/g;
4460ad90 106 $line =~ s/SPARSE_ARRAY_OF\([^)]+\)/int/g;
c952780c 107 $line =~ s/__declspec\([^)]+\)//;
121677b4
RS
108 if ( $line =~ /env (\S*)=/ ) {
109 # environment variable env NAME=...
110 $sym = $1;
111 } elsif ( $line =~ /typedef.*\(\*(\S+)\)\(.*/ ) {
0ed78e78
RL
112 # a callback function pointer: typedef ... (*NAME)(...
113 $sym = $1;
114 } elsif ( $line =~ /typedef.* (\S+)\(.*/ ) {
115 # a callback function signature: typedef ... NAME(...
121677b4
RS
116 $sym = $1;
117 } elsif ( $line =~ /typedef.* (\S+);/ ) {
118 # a simple typedef: typedef ... NAME;
8162f6f5 119 $sym = $1;
5d583521 120 } elsif ( $line =~ /enum (\S*) \{/ ) {
d4ea9659
RS
121 # an enumeration: enum ... {
122 $sym = $1;
0695b193 123 } elsif ( $line =~ /#(?:define|undef) ([A-Za-z0-9_]+)/ ) {
8162f6f5
RS
124 $sym = $1;
125 } elsif ( $line =~ /([A-Za-z0-9_]+)\(/ ) {
126 $sym = $1;
127 }
128 else {
129 next;
130 }
131 print "$id $sym missing from NAME section\n"
132 unless defined $names{$sym};
133 $names{$sym} = 2;
aebb9aac
RS
134
135 # Do some sanity checks on the prototype.
136 print "$id prototype missing spaces around commas: $line\n"
137 if ( $line =~ /[a-z0-9],[^ ]/ );
35ea640a
RS
138 }
139
140 foreach my $n ( keys %names ) {
141 next if $names{$n} == 2;
142 print "$id $n missing from SYNOPSIS\n";
143 }
144}
145
95f92d57
JL
146# Check if SECTION is located before BEFORE
147sub check_section_location()
cc838ee2
PY
148{
149 my $filename = shift;
150 my $contents = shift;
95f92d57
JL
151 my $section = shift;
152 my $before = shift;
cc838ee2 153
95f92d57
JL
154 return unless $contents =~ /=head1 $section/
155 and $contents =~ /=head1 $before/;
156 print "$filename: $section should be placed before $before section\n"
157 if $contents =~ /=head1 $before.*=head1 $section/ms;
cc838ee2
PY
158}
159
1bc74519
RS
160sub check()
161{
169a8e39
RL
162 my $filename = shift;
163 my $dirname = basename(dirname($filename));
843666ff 164
1bc74519
RS
165 my $contents = '';
166 {
167 local $/ = undef;
169a8e39 168 open POD, $filename or die "Couldn't open $filename, $!";
1bc74519
RS
169 $contents = <POD>;
170 close POD;
171 }
843666ff 172
95f92d57
JL
173 # Check if EXAMPLES is located after RETURN VALUES section.
174 &check_section_location($filename, $contents, "RETURN VALUES", "EXAMPLES") if $filename =~ m|man3/|;
573ac8f2
JL
175 # Check if HISTORY is located after SEE ALSO
176 &check_section_location($filename, $contents, "SEE ALSO", "HISTORY") if $filename =~ m|man3/|;
177 # Check if SEE ALSO is located after EXAMPLES
178 &check_section_location($filename, $contents, "EXAMPLES", "SEE ALSO") if $filename =~ m|man3/|;
cc838ee2 179
843666ff 180 my $id = "${filename}:1:";
35ea640a 181
4692340e 182 &name_synopsis($id, $filename, $contents)
8162f6f5 183 unless $contents =~ /=for comment generic/
99d63d46 184 or $filename =~ m@man[157]/@;
35ea640a
RS
185
186 print "$id doesn't start with =pod\n"
05ea606a 187 if $contents !~ /^=pod/;
35ea640a 188 print "$id doesn't end with =cut\n"
05ea606a 189 if $contents !~ /=cut\n$/;
35ea640a 190 print "$id more than one cut line.\n"
05ea606a 191 if $contents =~ /=cut.*=cut/ms;
35ea640a 192 print "$id missing copyright\n"
05ea606a 193 if $contents !~ /Copyright .* The OpenSSL Project Authors/;
35ea640a 194 print "$id copyright not last\n"
05ea606a 195 if $contents =~ /head1 COPYRIGHT.*=head/ms;
35ea640a 196 print "$id head2 in All uppercase\n"
843666ff 197 if $contents =~ /head2\s+[A-Z ]+\n/;
35ea640a
RS
198 print "$id extra space after head\n"
199 if $contents =~ /=head\d\s\s+/;
200 print "$id period in NAME section\n"
201 if $contents =~ /=head1 NAME.*\.\n.*=head1 SYNOPSIS/ms;
5a3371e2
RS
202 print "$id Duplicate $1 in L<>\n"
203 if $contents =~ /L<([^>]*)\|([^>]*)>/ && $1 eq $2;
e1271ac2 204 print "$id Bad =over $1\n"
2f61bc2e 205 if $contents =~ /=over([^ ][^24])/;
e90fc053
RS
206 print "$id Possible version style issue\n"
207 if $contents =~ /OpenSSL version [019]/;
843666ff 208
843666ff 209 if ( $contents !~ /=for comment multiple includes/ ) {
a95d7574
RS
210 # Look for multiple consecutive openssl #include lines
211 # (non-consecutive lines are okay; see man3/MD5.pod).
843666ff
RS
212 if ( $contents =~ /=head1 SYNOPSIS(.*)=head1 DESCRIPTION/ms ) {
213 my $count = 0;
214 foreach my $line ( split /\n+/, $1 ) {
215 if ( $line =~ m@include <openssl/@ ) {
a95d7574 216 print "$id has multiple includes\n" if ++$count == 2;
843666ff
RS
217 } else {
218 $count = 0;
219 }
220 }
221 }
222 }
05ea606a 223
35ea640a
RS
224 open my $OUT, '>', $temp
225 or die "Can't open $temp, $!";
169a8e39 226 podchecker($filename, $OUT);
35ea640a
RS
227 close $OUT;
228 open $OUT, '<', $temp
229 or die "Can't read $temp, $!";
230 while ( <$OUT> ) {
231 next if /\(section\) in.*deprecated/;
232 print;
233 }
234 close $OUT;
235 unlink $temp || warn "Can't remove $temp, $!";
a95d7574
RS
236
237 # Find what section this page is in; assume 3.
238 my $section = 3;
239 $section = $1 if $dirname =~ /man([1-9])/;
240
241 foreach ((@{$mandatory_sections{'*'}}, @{$mandatory_sections{$section}})) {
242 # Skip "return values" if not -s
a95d7574
RS
243 print "$id: missing $_ head1 section\n"
244 if $contents !~ /^=head1\s+${_}\s*$/m;
245 }
05ea606a 246}
1bc74519 247
71a8b855
RS
248my %dups;
249
250sub parsenum()
251{
252 my $file = shift;
253 my @apis;
254
255 open my $IN, '<', $file
256 or die "Can't open $file, $!, stopped";
257
258 while ( <$IN> ) {
274d1bee 259 next if /^#/;
71a8b855
RS
260 next if /\bNOEXIST\b/;
261 next if /\bEXPORT_VAR_AS_FUNC\b/;
1722496f
RS
262 my @fields = split();
263 die "Malformed line $_"
264 if scalar @fields != 2 && scalar @fields != 4;
265 push @apis, $fields[0];
71a8b855
RS
266 }
267
268 close $IN;
269
274d1bee 270 print "# Found ", scalar(@apis), " in $file\n" unless $opt_p;
71a8b855
RS
271 return sort @apis;
272}
273
23ab880d 274sub getdocced
71a8b855
RS
275{
276 my $dir = shift;
277 my %return;
278
279 foreach my $pod ( glob("$dir/*.pod") ) {
280 my %podinfo = extract_pod_info($pod);
281 foreach my $n ( @{$podinfo{names}} ) {
282 $return{$n} = $pod;
283 print "# Duplicate $n in $pod and $dups{$n}\n"
284 if defined $dups{$n} && $dups{$n} ne $pod;
285 $dups{$n} = $pod;
286 }
287 }
288
289 return %return;
290}
291
292my %docced;
293
9a2dfc0f
RS
294sub checkmacros()
295{
296 my $count = 0;
ee4afacd 297 my %seen;
9a2dfc0f
RS
298
299 print "# Checking macros (approximate)\n";
300 foreach my $f ( glob('include/openssl/*.h') ) {
301 # Skip some internals we don't want to document yet.
302 next if $f eq 'include/openssl/asn1.h';
303 next if $f eq 'include/openssl/asn1t.h';
304 next if $f eq 'include/openssl/err.h';
305 open(IN, $f) || die "Can't open $f, $!";
306 while ( <IN> ) {
307 next unless /^#\s*define\s*(\S+)\(/;
308 my $macro = $1;
ee4afacd 309 next if $docced{$macro} || defined $seen{$macro};
9a2dfc0f
RS
310 next if $macro =~ /i2d_/
311 || $macro =~ /d2i_/
312 || $macro =~ /DEPRECATEDIN/
313 || $macro =~ /IMPLEMENT_/
314 || $macro =~ /DECLARE_/;
8d50b9c1 315 print "$f:$macro\n" if $opt_d;
9a2dfc0f 316 $count++;
ee4afacd 317 $seen{$macro} = 1;
9a2dfc0f
RS
318 }
319 close(IN);
320 }
44e69951 321 print "# Found $count macros missing (not all should be documented)\n"
9a2dfc0f
RS
322}
323
71a8b855
RS
324sub printem()
325{
326 my $libname = shift;
327 my $numfile = shift;
328 my $count = 0;
ee4afacd 329 my %seen;
71a8b855
RS
330
331 foreach my $func ( &parsenum($numfile) ) {
ee4afacd 332 next if $docced{$func} || defined $seen{$func};
71a8b855
RS
333
334 # Skip ASN1 utilities
335 next if $func =~ /^ASN1_/;
336
8d50b9c1 337 print "$libname:$func\n" if $opt_d;
71a8b855 338 $count++;
ee4afacd 339 $seen{$func} = 1;
71a8b855
RS
340 }
341 print "# Found $count missing from $numfile\n\n";
342}
343
344
9e183d22
RS
345# Collection of links in each POD file.
346# filename => [ "foo(1)", "bar(3)", ... ]
347my %link_collection = ();
348# Collection of names in each POD file.
349# "name(s)" => filename
350my %name_collection = ();
351
352sub collectnames {
353 my $filename = shift;
354 $filename =~ m|man(\d)/|;
355 my $section = $1;
356 my $simplename = basename($filename, ".pod");
357 my $id = "${filename}:1:";
358
359 my $contents = '';
360 {
361 local $/ = undef;
362 open POD, $filename or die "Couldn't open $filename, $!";
363 $contents = <POD>;
364 close POD;
365 }
366
367 $contents =~ /=head1 NAME([^=]*)=head1 /ms;
368 my $tmp = $1;
369 unless (defined $tmp) {
370 print "$id weird name section\n";
371 return;
372 }
373 $tmp =~ tr/\n/ /;
374 $tmp =~ s/-.*//g;
375
23ab880d 376 my @names = map { s/^\s+//g; s/\s+$//g; $_ } split(/,/, $tmp);
9e183d22
RS
377 unless (grep { $simplename eq $_ } @names) {
378 print "$id missing $simplename\n";
379 push @names, $simplename;
380 }
381 foreach my $name (@names) {
382 next if $name eq "";
23ab880d
RL
383 if ($name =~ /\s/) {
384 print "$id '$name' contains white space\n";
385 }
9e183d22
RS
386 my $name_sec = "$name($section)";
387 if (! exists $name_collection{$name_sec}) {
388 $name_collection{$name_sec} = $filename;
389 } else { #elsif ($filename ne $name_collection{$name_sec}) {
390 print "$id $name_sec also in $name_collection{$name_sec}\n";
391 }
392 }
393
394 my @foreign_names =
395 map { map { s/\s+//g; $_ } split(/,/, $_) }
396 $contents =~ /=for\s+comment\s+foreign\s+manuals:\s*(.*)\n\n/;
397 foreach (@foreign_names) {
398 $name_collection{$_} = undef; # It still exists!
399 }
400
401 my @links = $contents =~ /L<
402 # if the link is of the form L<something|name(s)>,
403 # then remove 'something'. Note that 'something'
404 # may contain POD codes as well...
405 (?:(?:[^\|]|<[^>]*>)*\|)?
46f4e1be 406 # we're only interested in references that have
9e183d22
RS
407 # a one digit section number
408 ([^\/>\(]+\(\d\))
409 /gx;
410 $link_collection{$filename} = [ @links ];
411}
412
413sub checklinks {
414 foreach my $filename (sort keys %link_collection) {
415 foreach my $link (@{$link_collection{$filename}}) {
416 print "${filename}:1: reference to non-existing $link\n"
417 unless exists $name_collection{$link};
418 }
419 }
420}
421
274d1bee
RS
422sub publicize() {
423 foreach my $name ( &parsenum('util/libcrypto.num') ) {
424 $public{$name} = 1;
425 }
426 foreach my $name ( &parsenum('util/libssl.num') ) {
427 $public{$name} = 1;
428 }
429 foreach my $name ( &parsenum('util/private.num') ) {
430 $public{$name} = 1;
431 }
432}
433
e75138ab
RS
434my %skips = (
435 'aes128' => 1,
436 'aes192' => 1,
437 'aes256' => 1,
438 'aria128' => 1,
439 'aria192' => 1,
440 'aria256' => 1,
441 'camellia128' => 1,
442 'camellia192' => 1,
443 'camellia256' => 1,
444 'des' => 1,
445 'des3' => 1,
446 'idea' => 1,
447 '[cipher]' => 1,
448 '[digest]' => 1,
449);
450
451sub checkflags() {
452 my $cmd = shift;
453 my %cmdopts;
454 my %docopts;
455 my $ok = 1;
456
457 # Get the list of options in the command.
458 open CFH, "./apps/openssl list --options $cmd|"
459 || die "Can list options for $cmd, $!";
460 while ( <CFH> ) {
461 chop;
462 s/ .$//;
463 $cmdopts{$_} = 1;
464 }
465 close CFH;
466
467 # Get the list of flags from the synopsis
468 open CFH, "<doc/man1/$cmd.pod"
469 || die "Can't open $cmd.pod, $!";
470 while ( <CFH> ) {
471 chop;
472 last if /DESCRIPTION/;
473 next unless /\[B<-([^ >]+)/;
474 $docopts{$1} = 1;
475 }
476 close CFH;
477
478 # See what's in the command not the manpage.
479 my @undocced = ();
480 foreach my $k ( keys %cmdopts ) {
481 push @undocced, $k unless $docopts{$k};
482 }
483 if ( scalar @undocced > 0 ) {
484 $ok = 0;
485 foreach ( @undocced ) {
486 print "doc/man1/$cmd.pod: Missing -$_\n";
487 }
488 }
489
490 # See what's in the command not the manpage.
491 my @unimpl = ();
492 foreach my $k ( keys %docopts ) {
493 push @unimpl, $k unless $cmdopts{$k};
494 }
495 if ( scalar @unimpl > 0 ) {
496 $ok = 0;
497 foreach ( @unimpl ) {
498 next if defined $skips{$_};
499 print "doc/man1/$cmd.pod: Not implemented -$_\n";
500 }
501 }
502
503 return $ok;
504}
505
a085f43f 506getopts('cdlnphu');
274d1bee
RS
507
508&help() if $opt_h;
a085f43f 509$opt_n = 1 if $opt_p;
8d50b9c1 510$opt_u = 1 if $opt_d;
35ea640a 511
a085f43f 512die "Need one of -[cdlnpu] flags.\n"
e75138ab 513 unless $opt_c or $opt_l or $opt_n or $opt_u;
71a8b855 514
e75138ab
RS
515if ( $opt_c ) {
516 my $ok = 1;
517 my @commands = ();
3dfda1a6 518
e75138ab
RS
519 # Get list of commands.
520 open FH, "./apps/openssl list -1 -commands|"
521 || die "Can't list commands, $!";
522 while ( <FH> ) {
523 chop;
524 push @commands, $_;
525 }
526 close FH;
527
528 # See if each has a manpage.
529 foreach ( @commands ) {
530 next if $_ eq 'help' || $_ eq 'exit';
531 if ( ! -f "doc/man1/$_.pod" ) {
532 print "doc/man1/$_.pod does not exist\n";
533 $ok = 0;
534 } else {
535 $ok = 0 if not &checkflags($_);
536 }
71a8b855 537 }
e75138ab
RS
538
539 # See what help is missing.
540 open FH, "./apps/openssl list --missing-help |"
541 || die "Can't list missing help, $!";
542 while ( <FH> ) {
543 chop;
544 my ($cmd, $flag) = split;
545 print "$cmd has no help for -$flag\n";
546 $ok = 0;
547 }
548 close FH;
549
550 exit 1 if not $ok;
71a8b855 551}
9e183d22
RS
552
553if ( $opt_l ) {
23ab880d
RL
554 foreach (@ARGV ? @ARGV : (glob('doc/*/*.pod'),
555 glob('doc/internal/*/*.pod'))) {
9e183d22
RS
556 collectnames($_);
557 }
558 checklinks();
559}
560
e75138ab
RS
561if ( $opt_n ) {
562 &publicize() if $opt_p;
563 foreach (@ARGV ? @ARGV : glob('doc/*/*.pod')) {
564 &check($_);
565 }
23ab880d
RL
566 {
567 local $opt_p = undef;
568 foreach (@ARGV ? @ARGV : glob('doc/internal/*/*.pod')) {
569 &check($_);
570 }
571 }
e75138ab
RS
572}
573
71a8b855 574if ( $opt_u ) {
23ab880d 575 my %temp = getdocced('doc/man3');
71a8b855
RS
576 foreach ( keys %temp ) {
577 $docced{$_} = $temp{$_};
578 }
579 &printem('crypto', 'util/libcrypto.num');
580 &printem('ssl', 'util/libssl.num');
9a2dfc0f 581 &checkmacros();
1bc74519 582}
05ea606a 583
35ea640a 584exit;