xref: /linux/scripts/documentation-file-ref-check (revision 71e2f4dd5a65bd8dbca0b77661e75eea471168f8)
1#!/usr/bin/env perl
2# SPDX-License-Identifier: GPL-2.0
3#
4# Treewide grep for references to files under Documentation, and report
5# non-existing files in stderr.
6
7use warnings;
8use strict;
9use Getopt::Long qw(:config no_auto_abbrev);
10
11# NOTE: only add things here when the file was gone, but the text wants
12# to mention a past documentation file, for example, to give credits for
13# the original work.
14my %false_positives = (
15	"Documentation/scsi/scsi_mid_low_api.txt" => "Documentation/Configure.help",
16	"drivers/vhost/vhost.c" => "Documentation/virtual/lguest/lguest.c",
17);
18
19my $scriptname = $0;
20$scriptname =~ s,.*/([^/]+/),$1,;
21
22# Parse arguments
23my $help = 0;
24my $fix = 0;
25my $warn = 0;
26
27if (! -d ".git") {
28	printf "Warning: can't check if file exists, as this is not a git tree";
29	exit 0;
30}
31
32GetOptions(
33	'fix' => \$fix,
34	'warn' => \$warn,
35	'h|help|usage' => \$help,
36);
37
38if ($help != 0) {
39    print "$scriptname [--help] [--fix]\n";
40    exit -1;
41}
42
43# Step 1: find broken references
44print "Finding broken references. This may take a while...  " if ($fix);
45
46my %broken_ref;
47
48my $doc_fix = 0;
49
50open IN, "git grep ':doc:\`' Documentation/|"
51     or die "Failed to run git grep";
52while (<IN>) {
53	next if (!m,^([^:]+):.*\:doc\:\`([^\`]+)\`,);
54
55	my $d = $1;
56	my $doc_ref = $2;
57
58	my $f = $doc_ref;
59
60	$d =~ s,(.*/).*,$1,;
61	$f =~ s,.*\<([^\>]+)\>,$1,;
62
63	$f ="$d$f.rst";
64
65	next if (grep -e, glob("$f"));
66
67	if ($fix && !$doc_fix) {
68		print STDERR "\nWARNING: Currently, can't fix broken :doc:`` fields\n";
69	}
70	$doc_fix++;
71
72	print STDERR "$f: :doc:`$doc_ref`\n";
73}
74close IN;
75
76open IN, "git grep 'Documentation/'|"
77     or die "Failed to run git grep";
78while (<IN>) {
79	next if (!m/^([^:]+):(.*)/);
80
81	my $f = $1;
82	my $ln = $2;
83
84	# On linux-next, discard the Next/ directory
85	next if ($f =~ m,^Next/,);
86
87	# Makefiles and scripts contain nasty expressions to parse docs
88	next if ($f =~ m/Makefile/ || $f =~ m/\.sh$/);
89
90	# Skip this script
91	next if ($f eq $scriptname);
92
93	# Ignore the dir where documentation will be built
94	next if ($ln =~ m,\b(\S*)Documentation/output,);
95
96	if ($ln =~ m,\b(\S*)(Documentation/[A-Za-z0-9\_\.\,\~/\*\[\]\?+-]*)(.*),) {
97		my $prefix = $1;
98		my $ref = $2;
99		my $base = $2;
100		my $extra = $3;
101
102		# some file references are like:
103		# /usr/src/linux/Documentation/DMA-{API,mapping}.txt
104		# For now, ignore them
105		next if ($extra =~ m/^{/);
106
107		# Remove footnotes at the end like:
108		# Documentation/devicetree/dt-object-internal.txt[1]
109		$ref =~ s/(txt|rst)\[\d+]$/$1/;
110
111		# Remove ending ']' without any '['
112		$ref =~ s/\].*// if (!($ref =~ m/\[/));
113
114		# Remove puntuation marks at the end
115		$ref =~ s/[\,\.]+$//;
116
117		my $fulref = "$prefix$ref";
118
119		$fulref =~ s/^(\<file|ref)://;
120		$fulref =~ s/^[\'\`]+//;
121		$fulref =~ s,^\$\(.*\)/,,;
122		$base =~ s,.*/,,;
123
124		# Remove URL false-positives
125		next if ($fulref =~ m/^http/);
126
127		# Remove sched-pelt false-positive
128		next if ($fulref =~ m,^Documentation/scheduler/sched-pelt$,);
129
130		# Discard some build examples from Documentation/target/tcm_mod_builder.rst
131		next if ($fulref =~ m,mnt/sdb/lio-core-2.6.git/Documentation/target,);
132
133		# Check if exists, evaluating wildcards
134		next if (grep -e, glob("$ref $fulref"));
135
136		# Accept relative Documentation patches for tools/
137		if ($f =~ m/tools/) {
138			my $path = $f;
139			$path =~ s,(.*)/.*,$1,;
140			next if (grep -e, glob("$path/$ref $path/../$ref $path/$fulref"));
141		}
142
143		# Discard known false-positives
144		if (defined($false_positives{$f})) {
145			next if ($false_positives{$f} eq $fulref);
146		}
147
148		if ($fix) {
149			if (!($ref =~ m/(scripts|Kconfig|Kbuild)/)) {
150				$broken_ref{$ref}++;
151			}
152		} elsif ($warn) {
153			print STDERR "Warning: $f references a file that doesn't exist: $fulref\n";
154		} else {
155			print STDERR "$f: $fulref\n";
156		}
157	}
158}
159close IN;
160
161exit 0 if (!$fix);
162
163# Step 2: Seek for file name alternatives
164print "Auto-fixing broken references. Please double-check the results\n";
165
166foreach my $ref (keys %broken_ref) {
167	my $new =$ref;
168
169	my $basedir = ".";
170	# On translations, only seek inside the translations directory
171	$basedir  = $1 if ($ref =~ m,(Documentation/translations/[^/]+),);
172
173	# get just the basename
174	$new =~ s,.*/,,;
175
176	my $f="";
177
178	# usual reason for breakage: DT file moved around
179	if ($ref =~ /devicetree/) {
180		# usual reason for breakage: DT file renamed to .yaml
181		if (!$f) {
182			my $new_ref = $ref;
183			$new_ref =~ s/\.txt$/.yaml/;
184			$f=$new_ref if (-f $new_ref);
185		}
186
187		if (!$f) {
188			my $search = $new;
189			$search =~ s,^.*/,,;
190			$f = qx(find Documentation/devicetree/ -iname "*$search*") if ($search);
191			if (!$f) {
192				# Manufacturer name may have changed
193				$search =~ s/^.*,//;
194				$f = qx(find Documentation/devicetree/ -iname "*$search*") if ($search);
195			}
196		}
197	}
198
199	# usual reason for breakage: file renamed to .rst
200	if (!$f) {
201		$new =~ s/\.txt$/.rst/;
202		$f=qx(find $basedir -iname $new) if ($new);
203	}
204
205	# usual reason for breakage: use dash or underline
206	if (!$f) {
207		$new =~ s/[-_]/[-_]/g;
208		$f=qx(find $basedir -iname $new) if ($new);
209	}
210
211	# Wild guess: seek for the same name on another place
212	if (!$f) {
213		$f = qx(find $basedir -iname $new) if ($new);
214	}
215
216	my @find = split /\s+/, $f;
217
218	if (!$f) {
219		print STDERR "ERROR: Didn't find a replacement for $ref\n";
220	} elsif (scalar(@find) > 1) {
221		print STDERR "WARNING: Won't auto-replace, as found multiple files close to $ref:\n";
222		foreach my $j (@find) {
223			$j =~ s,^./,,;
224			print STDERR "    $j\n";
225		}
226	} else {
227		$f = $find[0];
228		$f =~ s,^./,,;
229		print "INFO: Replacing $ref to $f\n";
230		foreach my $j (qx(git grep -l $ref)) {
231			qx(sed "s\@$ref\@$f\@g" -i $j);
232		}
233	}
234}
235