2 # Copyright 2002-2019 The OpenSSL Project Authors. All Rights Reserved.
4 # Licensed under the Apache License 2.0 (the "License"). You may not use
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
16 use File::Spec::Functions;
18 use lib catdir(dirname($0), "perl");
19 use OpenSSL::Util::Pod;
21 my $debug = 0; # Set to 1 for debug output
38 Find small errors (nits) in documentation. Options:
39 -d Detailed list of undocumented (implies -u)
40 -e Detailed list of new undocumented (implies -v)
41 -s Same as -e except no output is generated if nothing is undocumented
42 -o Causes -e/-v to count symbols added since 1.1.1 as new (implies -v)
44 -n Print nits in POD pages
45 -p Warn if non-public name documented (implies -n)
46 -u Count undocumented functions
47 -v Count new undocumented functions
48 -h Print this help message
49 -c List undocumented commands and options
54 my $temp = '/tmp/docnits.txt';
59 my %mandatory_sections =
60 ( '*' => [ 'NAME', 'DESCRIPTION', 'COPYRIGHT' ],
61 1 => [ 'SYNOPSIS', 'OPTIONS' ],
62 3 => [ 'SYNOPSIS', 'RETURN VALUES' ],
66 # Print error message, set $status.
68 print join(" ", @_), "\n";
72 # Cross-check functions in the NAME and SYNOPSIS section.
78 # Get NAME section and all words in it.
79 return unless $contents =~ /=head1 NAME(.*)=head1 SYNOPSIS/ms;
82 err($id, "trailing comma before - in NAME")
85 err($id, "POD markup among the names in NAME")
88 err($id, "missing comma in NAME")
91 my $dirname = dirname($filename);
92 my $simplename = basename(basename($filename, ".in"), ".pod");
93 my $foundfilename = 0;
94 my %foundfilenames = ();
96 foreach my $n ( split ',', $tmp ) {
99 err($id, "the name '$n' contains white-space")
102 $foundfilename++ if $n eq $simplename;
103 $foundfilenames{$n} = 1
104 if ((-f "$dirname/$n.pod.in" || -f "$dirname/$n.pod")
105 && $n ne $simplename);
107 err($id, "the following exist as other .pod or .pod.in files:",
108 sort keys %foundfilenames)
110 err($id, "$simplename (filename) missing from NAME section")
111 unless $foundfilename;
112 foreach my $n ( keys %names ) {
113 err($id, "$n is not public")
114 if $opt_p and !defined $public{$n};
117 # Find all functions in SYNOPSIS
118 return unless $contents =~ /=head1 SYNOPSIS(.*)=head1 DESCRIPTION/ms;
120 foreach my $line ( split /\n+/, $syn ) {
121 next unless $line =~ /^\s/;
123 $line =~ s/STACK_OF\([^)]+\)/int/g;
124 $line =~ s/SPARSE_ARRAY_OF\([^)]+\)/int/g;
125 $line =~ s/__declspec\([^)]+\)//;
126 if ( $line =~ /env (\S*)=/ ) {
127 # environment variable env NAME=...
129 } elsif ( $line =~ /typedef.*\(\*(\S+)\)\(.*/ ) {
130 # a callback function pointer: typedef ... (*NAME)(...
132 } elsif ( $line =~ /typedef.* (\S+)\(.*/ ) {
133 # a callback function signature: typedef ... NAME(...
135 } elsif ( $line =~ /typedef.* (\S+);/ ) {
136 # a simple typedef: typedef ... NAME;
138 } elsif ( $line =~ /enum (\S*) \{/ ) {
139 # an enumeration: enum ... {
141 } elsif ( $line =~ /#(?:define|undef) ([A-Za-z0-9_]+)/ ) {
143 } elsif ( $line =~ /([A-Za-z0-9_]+)\(/ ) {
149 err($id, "$sym missing from NAME section")
150 unless defined $names{$sym};
153 # Do some sanity checks on the prototype.
154 err($id, "prototype missing spaces around commas: $line")
155 if ( $line =~ /[a-z0-9],[^ ]/ );
158 foreach my $n ( keys %names ) {
159 next if $names{$n} == 2;
160 err($id, "$n missing from SYNOPSIS")
164 # Check if SECTION ($3) is located before BEFORE ($4)
165 sub check_section_location {
167 my $contents = shift;
171 return unless $contents =~ /=head1 $section/
172 and $contents =~ /=head1 $before/;
173 err($id, "$section should appear before $before section")
174 if $contents =~ /=head1 $before.*=head1 $section/ms;
177 # Check if a =head1 is duplicated, or a =headX is duplicated within a
178 # =head1. Treats =head2 =head3 as equivalent -- it doesn't reset the head3
179 # sets if it finds a =head2 -- but that is good enough for now. Also check
180 # for proper capitalization, trailing periods, etc.
181 sub check_head_style {
183 my $contents = shift;
187 foreach my $line ( split /\n+/, $contents ) {
188 next unless $line =~ /^=head/;
189 if ( $line =~ /head1/ ) {
190 err($id, "duplicate section $line")
191 if defined $head1{$line};
195 err($id, "duplicate subsection $line")
196 if defined $subheads{$line};
197 $subheads{$line} = 1;
199 err($id, "period in =head")
200 if $line =~ /\.[^\w]/ or $line =~ /\.$/;
201 err($id, "not all uppercase in =head1")
202 if $line =~ /head1.*[a-z]/;
203 err($id, "all uppercase in subhead")
204 if $line =~ /head[234][ A-Z0-9]+$/;
208 # Because we have options and symbols with extra markup, we need
209 # to take that into account, so we need a regexp that extracts
210 # markup chunks, including recursive markup.
211 # please read up on /(?R)/ in perlre(1)
212 # (note: order is important, (?R) needs to come before .)
213 # (note: non-greedy is important, or something like 'B<foo> and B<bar>'
214 # will be captured as one item)
217 [BIL]< # The start of what we recurse on
218 (?:(?-1)|.)*? # recurse the whole regexp (refering to
219 # the last opened capture group, i.e. the
220 # start of this regexp), or pick next
221 # character. Do NOT be greedy!
222 > # The end of what we recurse on
223 )/x; # (the x allows this sort of split up regexp)
225 # Options must start with a dash, followed by a letter, possibly
226 # followed by letters, digits, dashes and underscores, and the last
227 # character must be a letter or a digit.
228 # We do also accept the single -? or -n, where n is a digit
231 \? # Single question mark
237 [[:alpha:]](?:[-_[:alnum:]]*?[[:alnum:]])?
240 # Helper function to check if a given $thing is properly marked up
241 # option. It returns one of these values:
243 # undef if it's not an option
244 # "" if it's a malformed option
245 # $unwrapped the option with the outermost B<> wrapping removed.
246 sub normalise_option {
248 my $filename = shift;
251 my $unwrapped = $thing;
252 my $unmarked = $thing;
254 # $unwrapped is the option with the outer B<> markup removed
255 $unwrapped =~ s/^B<//;
256 $unwrapped =~ s/>$//;
257 # $unmarked is the option with *all* markup removed
258 $unmarked =~ s/[BIL]<|>//msg;
261 # If we found an option, check it, collect it
262 if ( $unwrapped =~ /^\s*-/ ) {
263 return $unwrapped # return option with outer B<> removed
264 if $unmarked =~ /^-${option_re}$/;
265 return ""; # Malformed option
267 return undef; # Something else
270 # Checks of command option (man1) formatting. The man1 checks are
271 # restricted to the SYNOPSIS and OPTIONS sections, the rest is too
272 # free form, we simply cannot be too strict there.
276 my $filename = shift;
277 my $contents = shift;
279 my $synopsis = ($contents =~ /=head1\s+SYNOPSIS(.*?)=head1/s, $1);
281 # Some pages have more than one OPTIONS section, let's make sure
284 while ( $contents =~ /=head1\s+[A-Z ]*?OPTIONS$(.*?)(?==head1)/msg ) {
288 # Look for options with no or incorrect markup
290 /(?<![-<[:alnum:]])-(?:$markup_re|.)*(?![->[:alnum:]])/msg ) {
291 err($id, "Malformed option [1] in SYNOPSIS: $&");
294 while ( $synopsis =~ /$markup_re/msg ) {
296 print STDERR "$id:DEBUG[option_check] SYNOPSIS: found $found\n"
298 my $option_uw = normalise_option($id, $filename, $found);
299 err($id, "Malformed option [2] in SYNOPSIS: $found")
300 if defined $option_uw && $option_uw eq '';
303 # In OPTIONS, we look for =item paragraphs.
304 # (?=^\s*$) detects an empty line.
305 while ( $options =~ /=item\s+(.*?)(?=^\s*$)/msg ) {
308 while ( $item =~ /(\[\s*)?($markup_re)/msg ) {
310 print STDERR "$id:DEBUG[option_check] OPTIONS: found $&\n"
312 err($id, "Unexpected bracket in OPTIONS =item: $item")
313 if ($1 // '') ne '' && $found =~ /^B<\s*-/;
315 my $option_uw = normalise_option($id, $filename, $found);
316 err($id, "Malformed option in OPTIONS: $found")
317 if defined $option_uw && $option_uw eq '';
323 my $symbol_re = qr/[[:alpha:]_][_[:alnum:]]*?/;
325 # Checks of function name (man3) formatting. The man3 checks are
326 # easier than the man1 checks, we only check the names followed by (),
327 # and only the names that have POD markup.
329 sub functionname_check {
331 my $filename = shift;
332 my $contents = shift;
334 while ( $contents =~ /($markup_re)\(\)/msg ) {
335 print STDERR "$id:DEBUG[functionname_check] SYNOPSIS: found $&\n"
339 my $unmarked = $symbol;
340 $unmarked =~ s/[BIL]<|>//msg;
342 err($id, "Malformed symbol: $symbol")
343 unless $symbol =~ /^B<.*>$/ && $unmarked =~ /^${symbol_re}$/
346 # We can't do the kind of collecting coolness that option_check()
347 # does, because there are too many things that can't be found in
348 # name repositories like the NAME sections, such as symbol names
349 # with a variable part (typically marked up as B<foo_I<TYPE>_bar>
353 my $filename = shift;
354 my $dirname = basename(dirname($filename));
359 open POD, $filename or die "Couldn't open $filename, $!";
364 my $id = "${filename}:1:";
365 check_head_style($id, $contents);
367 # Check ordering of some sections in man3
368 if ( $filename =~ m|man3/| ) {
369 check_section_location($id, $contents, "RETURN VALUES", "EXAMPLES");
370 check_section_location($id, $contents, "SEE ALSO", "HISTORY");
371 check_section_location($id, $contents, "EXAMPLES", "SEE ALSO");
374 unless ( $contents =~ /=for comment generic/ ) {
375 if ( $filename =~ m|man3/| ) {
376 name_synopsis($id, $filename, $contents);
377 functionname_check($id, $filename, $contents);
378 } elsif ( $filename =~ m|man1/| ) {
379 option_check($id, $filename, $contents)
383 err($id, "doesn't start with =pod")
384 if $contents !~ /^=pod/;
385 err($id, "doesn't end with =cut")
386 if $contents !~ /=cut\n$/;
387 err($id, "more than one cut line.")
388 if $contents =~ /=cut.*=cut/ms;
389 err($id, "EXAMPLE not EXAMPLES section.")
390 if $contents =~ /=head1 EXAMPLE[^S]/;
391 err($id, "WARNING not WARNINGS section.")
392 if $contents =~ /=head1 WARNING[^S]/;
393 err($id, "missing copyright")
394 if $contents !~ /Copyright .* The OpenSSL Project Authors/;
395 err($id, "copyright not last")
396 if $contents =~ /head1 COPYRIGHT.*=head/ms;
397 err($id, "head2 in All uppercase")
398 if $contents =~ /head2\s+[A-Z ]+\n/;
399 err($id, "extra space after head")
400 if $contents =~ /=head\d\s\s+/;
401 err($id, "period in NAME section")
402 if $contents =~ /=head1 NAME.*\.\n.*=head1 SYNOPSIS/ms;
403 err($id, "Duplicate $1 in L<>")
404 if $contents =~ /L<([^>]*)\|([^>]*)>/ && $1 eq $2;
405 err($id, "Bad =over $1")
406 if $contents =~ /=over([^ ][^24])/;
407 err($id, "Possible version style issue")
408 if $contents =~ /OpenSSL version [019]/;
410 if ( $contents !~ /=for comment multiple includes/ ) {
411 # Look for multiple consecutive openssl #include lines
412 # (non-consecutive lines are okay; see man3/MD5.pod).
413 if ( $contents =~ /=head1 SYNOPSIS(.*)=head1 DESCRIPTION/ms ) {
415 foreach my $line ( split /\n+/, $1 ) {
416 if ( $line =~ m@include <openssl/@ ) {
417 err($id, "has multiple includes")
426 open my $OUT, '>', $temp
427 or die "Can't open $temp, $!";
428 podchecker($filename, $OUT);
430 open $OUT, '<', $temp
431 or die "Can't read $temp, $!";
433 next if /\(section\) in.*deprecated/;
437 unlink $temp || warn "Can't remove $temp, $!";
439 # Find what section this page is in; assume 3.
441 $section = $1 if $dirname =~ /man([1-9])/;
443 foreach ((@{$mandatory_sections{'*'}}, @{$mandatory_sections{$section}})) {
444 # Skip "return values" if not -s
445 err($id, "missing $_ head1 section")
446 if $contents !~ /^=head1\s+${_}\s*$/m;
456 open my $IN, '<', $file
457 or die "Can't open $file, $!, stopped";
461 next if /\bNOEXIST\b/;
462 my @fields = split();
463 die "Malformed line $_"
464 if scalar @fields != 2 && scalar @fields != 4;
465 push @apis, $fields[0];
470 print "# Found ", scalar(@apis), " in $file\n" unless $opt_p;
479 foreach my $pod ( glob("$dir/*.pod"), glob("$dir/*.pod.in") ) {
480 my %podinfo = extract_pod_info($pod);
481 foreach my $n ( @{$podinfo{names}} ) {
483 print "# Duplicate $n in $pod and $dups{$n}\n"
484 if defined $dups{$n} && $dups{$n} ne $pod;
496 my $missingfile = shift;
499 open FH, $missingfile
500 || die "Can't open $missingfile";
517 @missing = loadmissing('util/missingmacro111.txt');
519 @missing = loadmissing('util/missingmacro.txt');
522 print "# Checking macros (approximate)\n"
524 foreach my $f ( glob('include/openssl/*.h') ) {
525 # Skip some internals we don't want to document yet.
526 next if $f eq 'include/openssl/asn1.h';
527 next if $f eq 'include/openssl/asn1t.h';
528 next if $f eq 'include/openssl/err.h';
529 open(IN, $f) || die "Can't open $f, $!";
531 next unless /^#\s*define\s*(\S+)\(/;
533 next if $docced{$macro} || defined $seen{$macro};
534 next if $macro =~ /i2d_/
536 || $macro =~ /DEPRECATEDIN/
537 || $macro =~ /IMPLEMENT_/
538 || $macro =~ /DECLARE_/;
540 # Skip macros known to be missing
541 next if $opt_v && grep( /^$macro$/, @missing);
550 print "# Found $count macros missing\n"
551 if !$opt_s || $count > 0;
557 my $missingfile = shift;
561 my @missing = loadmissing($missingfile) if ($opt_v);
563 foreach my $func ( parsenum($numfile) ) {
564 next if $docced{$func} || defined $seen{$func};
566 # Skip ASN1 utilities
567 next if $func =~ /^ASN1_/;
569 # Skip functions known to be missing
570 next if $opt_v && grep( /^$func$/, @missing);
572 print "$libname:$func\n"
577 print "# Found $count missing from $numfile\n\n"
578 if !$opt_s || $count > 0;
582 # Collection of links in each POD file.
583 # filename => [ "foo(1)", "bar(3)", ... ]
584 my %link_collection = ();
585 # Collection of names in each POD file.
586 # "name(s)" => filename
587 my %name_collection = ();
590 my $filename = shift;
591 $filename =~ m|man(\d)/|;
593 my $simplename = basename(basename($filename, ".in"), ".pod");
594 my $id = "${filename}:1:";
599 open POD, $filename or die "Couldn't open $filename, $!";
604 $contents =~ /=head1 NAME([^=]*)=head1 /ms;
606 unless (defined $tmp) {
607 err($id, "weird name section");
614 map { s|/|-|g; $_ } # Treat slash as dash
615 map { s/^\s+//g; s/\s+$//g; $_ } # Trim prefix and suffix blanks
617 unless (grep { $simplename eq $_ } @names) {
618 err($id, "missing $simplename");
619 push @names, $simplename;
621 foreach my $name (@names) {
624 err($id, "'$name' contains white space")
626 my $name_sec = "$name($section)";
627 if (! exists $name_collection{$name_sec}) {
628 $name_collection{$name_sec} = $filename;
629 } elsif ($filename eq $name_collection{$name_sec}) {
630 err($id, "$name_sec repeated in NAME section of",
631 $name_collection{$name_sec});
633 err($id, "$name_sec also in NAME section of",
634 $name_collection{$name_sec});
639 map { map { s/\s+//g; $_ } split(/,/, $_) }
640 $contents =~ /=for\s+comment\s+foreign\s+manuals:\s*(.*)\n\n/;
641 foreach (@foreign_names) {
642 $name_collection{$_} = undef; # It still exists!
645 my @links = $contents =~ /L<
646 # if the link is of the form L<something|name(s)>,
647 # then remove 'something'. Note that 'something'
648 # may contain POD codes as well...
649 (?:(?:[^\|]|<[^>]*>)*\|)?
650 # we're only interested in references that have
651 # a one digit section number
654 $link_collection{$filename} = [ @links ];
658 foreach my $filename (sort keys %link_collection) {
659 foreach my $link (@{$link_collection{$filename}}) {
660 err("${filename}:1:", "reference to non-existing $link")
661 unless exists $name_collection{$link};
667 foreach my $name ( parsenum('util/libcrypto.num') ) {
670 foreach my $name ( parsenum('util/libssl.num') ) {
673 foreach my $name ( parsenum('util/private.num') ) {
678 # Cipher/digests to skip if not documented
703 # Get the list of options in the command.
704 open CFH, "./apps/openssl list --options $cmd|"
705 || die "Can list options for $cmd, $!";
713 # Get the list of flags from the synopsis
715 || die "Can't open $doc, $!";
718 last if /DESCRIPTION/;
719 if ( /=for comment ifdef (.*)/ ) {
720 foreach my $f ( split / /, $1 ) {
725 next unless /\[B<-([^ >]+)/;
727 $opt = $1 if $opt =~ /I<(.*)/;
732 # See what's in the command not the manpage.
734 foreach my $k ( keys %cmdopts ) {
735 push @undocced, $k unless $docopts{$k};
737 if ( scalar @undocced > 0 ) {
738 foreach ( @undocced ) {
739 err("$doc: undocumented option -$_");
743 # See what's in the command not the manpage.
745 foreach my $k ( keys %docopts ) {
746 push @unimpl, $k unless $cmdopts{$k};
748 if ( scalar @unimpl > 0 ) {
749 foreach ( @unimpl ) {
750 next if defined $skips{$_} || defined $localskips{$_};
751 err("$cmd documented but not implemented -$_");
756 getopts('cdesolnphuv');
760 $opt_n = 1 if $opt_p;
761 $opt_u = 1 if $opt_d;
762 $opt_e = 1 if $opt_s;
763 $opt_v = 1 if $opt_o || $opt_e;
765 die "Cannot use both -u and -v"
767 die "Cannot use both -d and -e"
770 # We only need to check c, l, n, u and v.
771 # Options d, e, s, o and p imply one of the above.
772 die "Need one of -[cdesolnpuv] flags.\n"
773 unless $opt_c or $opt_l or $opt_n or $opt_u or $opt_v;
778 # Get list of commands.
779 open FH, "./apps/openssl list -1 -commands|"
780 || die "Can't list commands, $!";
787 # See if each has a manpage.
788 foreach my $cmd ( @commands ) {
789 next if $cmd eq 'help' || $cmd eq 'exit';
790 my $doc = "doc/man1/$cmd.pod";
791 $doc = "doc/man1/openssl-$cmd.pod" if -f "doc/man1/openssl-$cmd.pod";
793 err("$doc does not exist");
795 checkflags($cmd, $doc);
799 # See what help is missing.
800 open FH, "./apps/openssl list --missing-help |"
801 || die "Can't list missing help, $!";
804 my ($cmd, $flag) = split;
805 err("$cmd has no help for -$flag");
813 foreach (@ARGV ? @ARGV : (glob('doc/*/*.pod'), glob('doc/*/*.pod.in'),
814 glob('doc/internal/*/*.pod'))) {
821 publicize() if $opt_p;
822 foreach (@ARGV ? @ARGV : (glob('doc/*/*.pod'), glob('doc/*/*.pod.in'))) {
826 local $opt_p = undef;
827 foreach (@ARGV ? @ARGV : glob('doc/internal/*/*.pod')) {
832 # If not given args, check that all man1 commands are named properly.
833 if ( scalar @ARGV == 0 ) {
834 foreach (glob('doc/man1/*.pod')) {
835 next if /CA.pl/ || /openssl.pod/;
836 err("$_ doesn't start with openssl-") unless /openssl-/;
841 if ( $opt_u || $opt_v) {
842 my %temp = getdocced('doc/man3');
843 foreach ( keys %temp ) {
844 $docced{$_} = $temp{$_};
847 printem('crypto', 'util/libcrypto.num', 'util/missingcrypto111.txt');
848 printem('ssl', 'util/libssl.num', 'util/missingssl111.txt');
850 printem('crypto', 'util/libcrypto.num', 'util/missingcrypto.txt');
851 printem('ssl', 'util/libssl.num', 'util/missingssl.txt');