xref: /curl/tests/test1173.pl (revision 46d7214c)
1#!/usr/bin/env perl
2#***************************************************************************
3#                                  _   _ ____  _
4#  Project                     ___| | | |  _ \| |
5#                             / __| | | | |_) | |
6#                            | (__| |_| |  _ <| |___
7#                             \___|\___/|_| \_\_____|
8#
9# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
10#
11# This software is licensed as described in the file COPYING, which
12# you should have received as part of this distribution. The terms
13# are also available at https://curl.se/docs/copyright.html.
14#
15# You may opt to use, copy, modify, merge, publish, distribute and/or sell
16# copies of the Software, and permit persons to whom the Software is
17# furnished to do so, under the terms of the COPYING file.
18#
19# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20# KIND, either express or implied.
21#
22# SPDX-License-Identifier: curl
23#
24###########################################################################
25#
26# Scan man page(s) and detect some simple and yet common formatting mistakes.
27#
28# Output all deviances to stderr.
29
30use strict;
31use warnings;
32use File::Basename;
33
34# get the file name first
35my $symbolsinversions=shift @ARGV;
36
37# we may get the dir roots pointed out
38my @manpages=@ARGV;
39my $errors = 0;
40
41my %docsdirs;
42my %optblessed;
43my %funcblessed;
44my @optorder = (
45    'NAME',
46    'SYNOPSIS',
47    'DESCRIPTION',
48     #'DEFAULT', # CURLINFO_ has no default
49    'PROTOCOLS',
50    'EXAMPLE',
51    'AVAILABILITY',
52    'RETURN VALUE',
53    'SEE ALSO'
54    );
55my @funcorder = (
56    'NAME',
57    'SYNOPSIS',
58    'DESCRIPTION',
59    'EXAMPLE',
60    'AVAILABILITY',
61    'RETURN VALUE',
62    'SEE ALSO'
63    );
64my %shline; # section => line number
65
66my %symbol;
67
68# some CURLINFO_ symbols are not actual options for curl_easy_getinfo,
69# mark them as "deprecated" to hide them from link-warnings
70my %deprecated = (
71    CURLINFO_TEXT => 1,
72    CURLINFO_HEADER_IN => 1,
73    CURLINFO_HEADER_OUT => 1,
74    CURLINFO_DATA_IN => 1,
75    CURLINFO_DATA_OUT => 1,
76    CURLINFO_SSL_DATA_IN => 1,
77    CURLINFO_SSL_DATA_OUT => 1,
78    CURLOPT_EGDSOCKET => 1,
79    CURLOPT_RANDOM_FILE => 1,
80    );
81sub allsymbols {
82    open(my $f, "<", "$symbolsinversions") ||
83        die "$symbolsinversions: $|";
84    while(<$f>) {
85        if($_ =~ /^([^ ]*) +(.*)/) {
86            my ($name, $info) = ($1, $2);
87            $symbol{$name}=$name;
88
89            if($info =~ /([0-9.]+) +([0-9.]+)/) {
90                $deprecated{$name}=$info;
91            }
92        }
93    }
94    close($f);
95}
96
97
98my %ref = (
99    'curl.1' => 1
100    );
101sub checkref {
102    my ($f, $sec, $file, $line)=@_;
103    my $present = 0;
104    #print STDERR "check $f.$sec\n";
105    if($ref{"$f.$sec"}) {
106        # present
107        return;
108    }
109    foreach my $d (keys %docsdirs) {
110        if( -f "$d/$f.$sec") {
111            $present = 1;
112            $ref{"$f.$sec"}=1;
113            last;
114        }
115    }
116    if(!$present) {
117        print STDERR "$file:$line broken reference to $f($sec)\n";
118        $errors++;
119    }
120}
121
122sub scanmanpage {
123    my ($file) = @_;
124    my $reqex = 0;
125    my $inseealso = 0;
126    my $inex = 0;
127    my $insynop = 0;
128    my $exsize = 0;
129    my $synopsize = 0;
130    my $shc = 0;
131    my $optpage = 0; # option or function
132    my @sh;
133    my $SH="";
134    my @separators;
135    my @sepline;
136
137    open(my $m, "<", "$file") ||
138        die "test1173.pl could not open $file";
139    if($file =~ /[\/\\](CURL|curl_)([^\/\\]*).3/) {
140        # This is a man page for libcurl. It requires an example unless it's
141        # considered deprecated.
142        $reqex = 1 unless defined $deprecated{'CURL'.$2};
143        if($1 eq "CURL") {
144            $optpage = 1;
145        }
146    }
147    my $line = 1;
148    while(<$m>) {
149        chomp;
150        if($_ =~ /^.so /) {
151            # this man page is just a referral
152            close($m);
153            return;
154        }
155        if(($_ =~ /^\.SH SYNOPSIS/i) && ($reqex)) {
156            # this is for libcurl man page SYNOPSIS checks
157            $insynop = 1;
158            $inex = 0;
159        }
160        elsif($_ =~ /^\.SH EXAMPLE/i) {
161            $insynop = 0;
162            $inex = 1;
163        }
164        elsif($_ =~ /^\.SH \"SEE ALSO\"/i) {
165            $inseealso = 1;
166        }
167        elsif($_ =~ /^\.SH/i) {
168            $insynop = 0;
169            $inex = 0;
170        }
171        elsif($inseealso) {
172            if($_ =~ /^\.BR (.*)/i) {
173                my $f = $1;
174                if($f =~ /^(lib|)curl/i) {
175                    $f =~ s/[\n\r]//g;
176                    if($f =~ s/([a-z_0-9-]*) \(([13])\)([, ]*)//i) {
177                        push @separators, $3;
178                        push @sepline, $line;
179                        checkref($1, $2, $file, $line);
180                    }
181                    if($f !~ /^ *$/) {
182                        print STDERR "$file:$line bad SEE ALSO format\n";
183                        $errors++;
184                    }
185                }
186                else {
187                    if($f =~ /.*(, *)\z/) {
188                        push @separators, $1;
189                        push @sepline, $line;
190                    }
191                    else {
192                        push @separators, " ";
193                        push @sepline, $line;
194                    }
195                }
196            }
197        }
198        elsif($inex)  {
199            $exsize++;
200            if($_ =~ /[^\\]\\n/) {
201                print STDERR "$file:$line '\\n' need to be '\\\\n'!\n";
202            }
203        }
204        elsif($insynop)  {
205            $synopsize++;
206            if(($synopsize == 1) && ($_ !~ /\.nf/)) {
207                print STDERR "$file:$line:1:ERROR: be .nf for proper formatting\n";
208            }
209        }
210        if($_ =~ /^\.SH ([^\r\n]*)/i) {
211            my $n = $1;
212            # remove enclosing quotes
213            $n =~ s/\"(.*)\"\z/$1/;
214            push @sh, $n;
215            $shline{$n} = $line;
216            $SH = $n;
217        }
218
219        if($_ =~ /^\'/) {
220            print STDERR "$file:$line line starts with single quote!\n";
221            $errors++;
222        }
223        if($_ =~ /\\f([BI])(.*)/) {
224            my ($format, $rest) = ($1, $2);
225            if($rest !~ /\\fP/) {
226                print STDERR "$file:$line missing \\f${format} terminator!\n";
227                $errors++;
228            }
229        }
230        my $c = $_;
231        while($c =~ s/\\f([BI])((lib|)curl[a-z_0-9-]*)\(([13])\)//i) {
232            checkref($2, $4, $file, $line);
233        }
234        if(($_ =~ /\\f([BI])((libcurl|CURLOPT_|CURLSHOPT_|CURLINFO_|CURLMOPT_|curl_easy_|curl_multi_|curl_url|curl_mime|curl_global|curl_share)[a-zA-Z_0-9-]+)(.)/) &&
235           ($4 ne "(")) {
236            print STDERR "$file:$line curl ref to $2 without section\n";
237            $errors++;
238        }
239        if($_ =~ /(.*)\\f([^BIP])/) {
240            my ($pre, $format) = ($1, $2);
241            if($pre !~ /\\\z/) {
242                # only if there wasn't another backslash before the \f
243                print STDERR "$file:$line suspicious \\f format!\n";
244                $errors++;
245            }
246        }
247        if(($SH =~ /^(DESCRIPTION|RETURN VALUE|AVAILABILITY)/i) &&
248           ($_ =~ /(.*)((curl_multi|curl_easy|curl_url|curl_global|curl_url|curl_share)[a-zA-Z_0-9-]+)/) &&
249           ($1 !~ /\\fI$/)) {
250            print STDERR "$file:$line unrefed curl call: $2\n";
251            $errors++;
252        }
253
254
255        if($optpage && $SH && ($SH !~ /^(SYNOPSIS|EXAMPLE|NAME|SEE ALSO)/i) &&
256           ($_ =~ /(.*)(CURL(OPT_|MOPT_|INFO_|SHOPT_)[A-Z0-9_]*)/)) {
257            # an option with its own man page, check that it is tagged
258            # for linking
259            my ($pref, $symbol) = ($1, $2);
260            if($deprecated{$symbol}) {
261                # let it be
262            }
263            elsif($pref !~ /\\fI\z/) {
264                print STDERR "$file:$line option $symbol missing \\fI tagging\n";
265                $errors++;
266            }
267        }
268        if($_ =~ /[ \t]+$/) {
269            print STDERR "$file:$line trailing whitespace\n";
270            $errors++;
271        }
272        $line++;
273    }
274    close($m);
275
276    if(@separators) {
277        # all except the last one need comma
278        for(0 .. $#separators - 1) {
279            my $l = $_;
280            my $sep = $separators[$l];
281            if($sep ne ",") {
282                printf STDERR "$file:%d: bad not-last SEE ALSO separator: '%s'\n",
283                    $sepline[$l], $sep;
284                $errors++;
285            }
286        }
287        # the last one should not do comma
288        my $sep = $separators[$#separators];
289        if($sep eq ",") {
290            printf STDERR "$file:%d: superfluous comma separator\n",
291                $sepline[$#separators];
292            $errors++;
293        }
294    }
295
296    if($reqex) {
297        # only for libcurl options man-pages
298
299        my $shcount = scalar(@sh); # before @sh gets shifted
300        if($exsize < 2) {
301            print STDERR "$file:$line missing EXAMPLE section\n";
302            $errors++;
303        }
304
305        if($shcount < 3) {
306            print STDERR "$file:$line too few man page sections!\n";
307            $errors++;
308            return;
309        }
310
311        my $got = "start";
312        my $i = 0;
313        my $shused = 1;
314        my @shorig = @sh;
315        my @order = $optpage ? @optorder : @funcorder;
316        my $blessed = $optpage ? \%optblessed : \%funcblessed;
317
318        while($got) {
319            my $finesh;
320            $got = shift(@sh);
321            if($got) {
322                if($$blessed{$got}) {
323                    $i = $$blessed{$got};
324                    $finesh = $got; # a mandatory one
325                }
326            }
327            if($i && defined($finesh)) {
328                # mandatory section
329
330                if($i != $shused) {
331                    printf STDERR "$file:%u Got %s, when %s was expected\n",
332                        $shline{$finesh},
333                        $finesh,
334                        $order[$shused-1];
335                    $errors++;
336                    return;
337                }
338                $shused++;
339                if($i == scalar(@order)) {
340                    # last mandatory one, exit
341                    last;
342                }
343            }
344        }
345
346        if($i != scalar(@order)) {
347            printf STDERR "$file:$line missing mandatory section: %s\n",
348                $order[$i];
349            printf STDERR "$file:$line section found at index %u: '%s'\n",
350                $i, $shorig[$i];
351            printf STDERR " Found %u used sections\n", $shcount;
352            $errors++;
353        }
354    }
355}
356
357allsymbols();
358
359if(!$symbol{'CURLALTSVC_H1'}) {
360    print STDERR "didn't get the symbols-in-version!\n";
361    exit;
362}
363
364my $ind = 1;
365for my $s (@optorder) {
366    $optblessed{$s} = $ind++
367}
368$ind = 1;
369for my $s (@funcorder) {
370    $funcblessed{$s} = $ind++
371}
372
373for my $m (@manpages) {
374    $docsdirs{dirname($m)}++;
375}
376
377for my $m (@manpages) {
378    scanmanpage($m);
379}
380
381print STDERR "ok\n" if(!$errors);
382
383exit $errors;
384