While not all EXPORT_SYMBOL*() symbols should be documented,
it seems useful to have a tool which would help to check what
symbols aren't documented.

This is a first step on this direction. The tool has some
limitations. Yet, it could be useful for maintainers to check
about missing documents on their subsystems.

Suggested-by: Matthew Wilcox <[email protected]>
Signed-off-by: Mauro Carvalho Chehab <[email protected]>
---
 scripts/check_docs_external_symbols | 267 ++++++++++++++++++++++++++++
 1 file changed, 267 insertions(+)
 create mode 100755 scripts/check_docs_external_symbols

diff --git a/scripts/check_docs_external_symbols 
b/scripts/check_docs_external_symbols
new file mode 100755
index 000000000000..cfa49015ded3
--- /dev/null
+++ b/scripts/check_docs_external_symbols
@@ -0,0 +1,267 @@
+#!/usr/bin/perl
+# SPDX-License-Identifier: GPL-2.0
+
+#
+# Copyright (c) 2020, Huawei Tech. Co., Ltd.
+# Author: Mauro Carvalho Chehab <[email protected]
+#
+# This script helps to check if exported functions are documented at either
+# a file, on at the included headers.
+#
+# The script uses some heuristics, being greedy when checking for includes
+# that aren't found at the normal place. This makes the script slower,
+# but increases the chance of finding a documentation for the symbol.
+#
+# Please always check the results of the script.
+#
+# Usage:
+#      scripts/check_external docs
+# or:
+#      scripts/check_external docs <files and/or directories>
+
+use warnings;
+use strict;
+use File::Find;
+use Cwd 'abs_path';
+
+use Data::Dumper;
+
+sub check_kerneldoc_symbols($$$$) {
+       my $file = shift;
+       my $docfile = shift;
+       my $exp = shift;
+       my $h = shift;
+       my @exports = @{$exp};
+       my %hash = %{$h};
+       my $is_kerneldoc = 0;
+       my $kerneldoc;
+       my $type;
+       my ($indent, $tag_indent);
+       my (@yes_symbols, @no_symbols);
+
+       $file = abs_path($file);
+
+       open IN, $docfile or return 0;
+       while (<IN>) {
+               while (s/\t+/' ' x (length($&) * 8 - length($`) % 8)/e) {};
+               if (/^(\s*)\.\.\s+kernel-doc\:\:\s*(\S+)/) {
+                       $indent = $1;
+                       $kerneldoc = abs_path($2);
+                       $is_kerneldoc = 1;
+                       @yes_symbols = @exports;
+                       @no_symbols = ();
+                       $tag_indent = "";
+                       next;
+               }
+
+               next if (!$is_kerneldoc);
+
+               if ($tag_indent ne "" && /^($tag_indent)\s+(.*)/) {
+                       my @sym = split /\s+/, $2;
+                       if ($type eq "identifiers") {
+                               push @yes_symbols, @sym;
+                       } else {
+                               push @no_symbols, @sym;
+                       }
+
+                       next;
+               }
+
+               if (/^($indent)\S/) {
+                       $is_kerneldoc = 0;
+
+                       if ($kerneldoc eq $file) {
+                               $hash{$_}++ for (@yes_symbols);
+                               $hash{$_}-- for (@no_symbols);
+                       }
+
+                       next;
+               }
+
+               next if ($kerneldoc ne $file);
+
+               # Check for kernel-doc variants
+
+               if (m/:internal:/) {
+                       @yes_symbols = ();
+                       next;
+               }
+               if (m/:export:/) {
+                       # Nothing to do here, as exports are covered
+                       next;
+               }
+
+               # Those are more painful to handle
+               if (m/^(\s+):identifiers:\s*(.*)/ || 
m/^(\s+):functions:\s*(.*)/) {
+                       $tag_indent = $1;
+                       $type = "identifiers";
+                       @yes_symbols = split /\s+/, $2;
+
+                       next;
+               }
+               if (m/^(\s+):no-identifiers:\s*(.*)/) {
+                       $type = "no-identifiers";
+                       $tag_indent = $1;
+                       @no_symbols = split /\s+/, $2;
+
+                       next;
+               }
+       }
+
+       if ($is_kerneldoc) {
+               if ($kerneldoc eq $file) {
+                       $hash{$_}++ for (@yes_symbols);
+                       $hash{$_}-- for (@no_symbols);
+               }
+       }
+
+
+       close IN;
+
+       return %hash;
+}
+
+sub check_file($) {
+       my $file = shift;
+       my (@files, @exports, @doc, @doc_refs, %file_exports, %hash);
+       my $content = "\n";
+
+       $file =~ s/\s+$//;
+
+       return 0 if (!($file =~ /\.[ch]$/));
+
+       my $dir = $file;
+       $dir =~ s,[^\/]+$,,;
+
+       open IN, $file or return 0;
+       while (<IN>) {
+               push @exports, $1 if (m/^EXPORT_SYMBOL.*\(\s*(\S+)\s*\)/);
+
+               if (m/^\s*#\s*include\s+[\<](\S+)[\>]/) {
+                       if (-e "include/uapi/$1") {
+                               push @files, "include/uapi/$1";
+                       } elsif (-e "include/$1") {
+                               push @files, "include/$1";
+                       } else {
+                               my @inc = split /\s+/,qx(git ls-files|grep $1);
+                               push @files, @inc;
+                       }
+               }
+               if (m/^\s*#\s*include\s+[\"](\S+)[\"]/) {
+                       if (-e "$dir/$1") {
+                               push @files, "$dir/$1";
+                       } else {
+                               my @inc = split /\s+/,qx(git ls-files|grep $1);
+                               push @files, @inc;
+                       }
+               }
+               $content .= $_;
+       }
+       close IN;
+
+       return 0 if ($content eq "\n");
+
+       push @files, $file;
+       my $has_docs = 0;
+       for (my $i = 0; $i < scalar(@files); $i++) {
+               $doc_refs[$i] = 0;
+               $file_exports{$files[$i]} = ();
+               $doc[$i] = qx(./scripts/kernel-doc --sphinx-version 3.2.1 
$files[$i] 2>/dev/null);
+               $has_docs =1 if ($doc[$i] ne "");
+       }
+
+       #
+       # Shortcut: if neither the file nor any headers has kernel-doc
+       # markups, there's no need to do anything else.
+       #
+       if (!$has_docs) {
+               print "warning: $file: has exports but no documentation\n";
+               return 1;
+       }
+
+       my @missing_exports;
+       my $found = -1;
+       my $num_not_functions = 0;
+       foreach my $e (@exports) {
+               # Check if the symbol is a function
+               if (!($content =~ 
(m/\n\s*(?:\w+\s+){0,}\*?\s*\b\Q$e\E\b\s*\(/))) {
+                       $num_not_functions++;
+                       next;
+               }
+               for (my $i = 0; $i < scalar(@files); $i++) {
+                       if ($doc[$i] =~ m/\b\Q$e\E\b/) {
+                               $found = $i;
+                               $doc_refs[$i]++;
+                               last;
+                       }
+                       push @{$file_exports{$files[$i]}}, $e;
+               }
+
+               push @missing_exports, $e if ($found < 0);
+       }
+
+       if (scalar(@exports) == scalar(@missing_exports) + $num_not_functions) {
+               print "warning: $file: has exports but no documentation\n";
+               return 1;
+       }
+
+       if (@missing_exports) {
+               print "warning: $file: missing documentation for 
@missing_exports\n";
+       }
+
+       for (my $i = 0; $i < scalar(@files); $i++) {
+               next if (!$doc_refs[$i]);
+               $hash{$_} = 0 for (@{$file_exports{$files[$i]}});
+               my @includes = split /\s+/, qx(git grep -l 
"kernel-doc::\\s*$files[$i]" Documentation/);
+
+               if (!@includes) {
+                       printf "warning: %s: file not included at 
Documentation/\n",
+                              $files[$i];
+                       return 1;
+               }
+
+               # Parse the $includes files, in order to check for
+               # excluded symbols
+
+               foreach my $inc (@includes) {
+                       %hash = check_kerneldoc_symbols($files[$i], $inc,
+                                                       
\@{$file_exports{$files[$i]}}, \%hash);
+               }
+               foreach my $s (keys %hash) {
+                       if ($hash{$s} < 1) {
+                               print "$files[$i]: export symbol $s not 
documented at: @includes\n";
+                       } elsif ($hash{$s} > 1) {
+                               print "$files[$i]: export symbol $s was 
documented %d times\n",
+                                   $hash{$s};
+                       }
+               }
+       }
+
+       return 1;
+}
+
+sub parse_dir {
+       check_file $File::Find::name;
+}
+
+#
+# main
+#
+
+if (@ARGV) {
+       while (@ARGV) {
+               my $file = shift;
+
+               if (-d $file) {
+                       find({wanted => \&parse_dir, no_chdir => 1}, $file);
+               } else {
+                       check_file $file;
+               }
+       }
+       exit;
+} else {
+       my @files = qx(git grep -l EXPORT_SYMBOL);
+       foreach my $file (@files) {
+               check_file $file;
+       }
+}
-- 
2.26.2

Reply via email to