]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
kconfig: add kconfig-sym-check static checker
authorAndrew Jones <andrew.jones@linux.dev>
Wed, 27 May 2026 14:27:03 +0000 (09:27 -0500)
committerNathan Chancellor <nathan@kernel.org>
Thu, 4 Jun 2026 02:20:04 +0000 (19:20 -0700)
Add 'make kconfig-sym-check', a static checker that finds Kconfig
symbols referenced in expressions (select, depends on, default, etc.)
but never defined via config/menuconfig anywhere in the tree. New
dangling symbols are reported as errors (exit 1) unless they are
listed in an exclusion file, e.g.

 KCONFIG_SYM_CHECK_EXCLUDES=sym-check-excludes make kconfig-sym-check

The exclusion file lists one symbol per line; blank lines and lines
starting with '#' are ignored.

The checker also warns about uppercase N/Y/M used as tristate literal
values following the same logic as checkpatch.

This new static checker is the script used for [1] with a few
improvements to avoid some false positives.

Link: https://bugzilla.kernel.org/show_bug.cgi?id=216748
Assisted-by: Claude:claude-sonnet-4-6
Signed-off-by: Andrew Jones <andrew.jones@linux.dev>
Acked-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com>
Acked-by: Randy Dunlap <rdunlap@infradead.org>
Tested-by: Randy Dunlap <rdunlap@infradead.org>
Tested-by: Julian Braha <julianbraha@gmail.com>
Tested-by: Nicolas Schier <nsc@kernel.org>
Acked-by: Nicolas Schier <nsc@kernel.org>
Link: https://patch.msgid.link/20260527142703.107110-1-andrew.jones@linux.dev
Signed-off-by: Nathan Chancellor <nathan@kernel.org>
Makefile
scripts/kconfig/kconfig-sym-check.pl [new file with mode: 0755]

index 857f08dcc952382672c71b714a46d4d1698961d8..37fdb454b6371a45bbb2b9d30ca1c03f82eb1dda 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -293,6 +293,7 @@ version_h := include/generated/uapi/linux/version.h
 clean-targets := %clean mrproper cleandocs
 no-dot-config-targets := $(clean-targets) \
                         cscope gtags TAGS tags help% %docs check% coccicheck \
+                        kconfig-sym-check \
                         $(version_h) headers headers_% archheaders archscripts \
                         %asm-generic kernelversion %src-pkg dt_binding_check \
                         outputmakefile rustavailable rustfmt rustfmtcheck \
@@ -1800,14 +1801,15 @@ help:
         echo  '                    (default: $(INSTALL_HDR_PATH))'; \
         echo  ''
        @echo  'Static analysers:'
-       @echo  '  checkstack      - Generate a list of stack hogs and consider all functions'
-       @echo  '                    with a stack size larger than MINSTACKSIZE (default: 100)'
-       @echo  '  versioncheck    - Sanity check on version.h usage'
-       @echo  '  includecheck    - Check for duplicate included header files'
-       @echo  '  headerdep       - Detect inclusion cycles in headers'
-       @echo  '  coccicheck      - Check with Coccinelle'
-       @echo  '  clang-analyzer  - Check with clang static analyzer'
-       @echo  '  clang-tidy      - Check with clang-tidy'
+       @echo  '  checkstack        - Generate a list of stack hogs and consider all functions'
+       @echo  '                      with a stack size larger than MINSTACKSIZE (default: 100)'
+       @echo  '  versioncheck      - Sanity check on version.h usage'
+       @echo  '  includecheck      - Check for duplicate included header files'
+       @echo  '  headerdep         - Detect inclusion cycles in headers'
+       @echo  '  coccicheck        - Check with Coccinelle'
+       @echo  '  kconfig-sym-check - Check for dangling Kconfig symbol references'
+       @echo  '  clang-analyzer    - Check with clang static analyzer'
+       @echo  '  clang-tidy        - Check with clang-tidy'
        @echo  ''
        @echo  'Tools:'
        @echo  '  nsdeps          - Generate missing symbol namespace dependencies'
@@ -2227,7 +2229,7 @@ endif
 # Scripts to check various things for consistency
 # ---------------------------------------------------------------------------
 
-PHONY += includecheck versioncheck coccicheck
+PHONY += includecheck versioncheck coccicheck kconfig-sym-check
 
 includecheck:
        find $(srctree)/* $(RCS_FIND_IGNORE) \
@@ -2242,6 +2244,9 @@ versioncheck:
 coccicheck:
        $(Q)$(BASH) $(srctree)/scripts/$@
 
+kconfig-sym-check:
+       $(Q)$(PERL) $(srctree)/scripts/kconfig/kconfig-sym-check.pl $(srctree) $(KCONFIG_SYM_CHECK_EXCLUDES)
+
 PHONY += checkstack kernelrelease kernelversion image_name
 
 # UML needs a little special treatment here.  It wants to use the host
diff --git a/scripts/kconfig/kconfig-sym-check.pl b/scripts/kconfig/kconfig-sym-check.pl
new file mode 100755 (executable)
index 0000000..daa5285
--- /dev/null
@@ -0,0 +1,132 @@
+#!/usr/bin/env perl
+# SPDX-License-Identifier: GPL-2.0
+
+use warnings;
+use strict;
+
+my $srctree = shift @ARGV;
+unless (defined $srctree) {
+       $srctree = `git rev-parse --show-toplevel 2>/dev/null`;
+       chomp $srctree;
+       my $msg = "Usage: $0 <srctree> [excludes file]\n";
+       $msg .= "Please provide <srctree>.";
+       $msg .= " Is it '$srctree'?" if $srctree;
+       $msg .= "\n";
+       die $msg;
+}
+my $kconfig_sym_check_excludes = defined $ARGV[0] ? $ARGV[0] : undef;
+
+sub indent_depth {
+       my ($ws) = @_;
+       my $col = 0;
+       for my $c (split //, $ws) {
+               $col = $c eq "\t" ? int($col / 8) * 8 + 8 : $col + 1;
+       }
+       return $col;
+}
+
+my @files = `git -C \Q$srctree\E ls-files '*Kconfig*' 2>/dev/null`;
+if (@files) {
+       chomp @files;
+       @files = map { "$srctree/$_" } @files;
+} else {
+       @files = `find \Q$srctree\E -name '*Kconfig*'`;
+       chomp @files;
+}
+
+@files = grep { !m{/scripts/kconfig/tests/} } @files;
+
+my %configs = ();
+my %refs = ();
+
+foreach my $file (@files) {
+       open F, $file or die "Cannot open $file: $!";
+
+       my $help = 0;
+       my $help_level;
+       my $level;
+
+       while (<F>) {
+               chomp;
+
+               while (/\\\s*$/) {
+                       s/\\\s*$/ /;
+                       my $cont = <F> // last;
+                       chomp $cont;
+                       $_ .= $cont;
+               }
+
+               next if /^\s*$/;
+               next if /^\s*#/;
+
+               /^(\s*)/;
+               $level = indent_depth($1);
+
+               if ($help && $level < $help_level) {
+                       $help = 0;
+               }
+
+               next if ($help);
+
+               if (/^\s*(help|\-\-\-help\-\-\-)$/) {
+                       $help = 1;
+                       my $next;
+                       while (defined($next = <F>)) {
+                               last unless $next =~ /^\s*(?:#.*)?$/;
+                       }
+                       last unless defined $next;
+                       $next =~ /^(\s*)/;
+                       if (indent_depth($1) >= $level) {
+                               $help_level = indent_depth($1);
+                       } else {
+                               $help = 0;
+                       }
+                       $_ = $next;
+                       redo;
+               }
+
+               if (/^\s*(config|menuconfig)\s+([a-zA-Z0-9_]+)\s*(#.*)?$/) {
+                       $configs{$2}++;
+                       next;
+               }
+
+               if (/^\s*(default|def_bool|def_tristate|select|depends\s+on|imply|visible\s+if|range|if|bool|tristate|int|hex|string|prompt)\s+(.+)\s*$/) {
+                       my $s = $2;
+                       $s =~ s/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'//g;
+                       $s =~ s/#.*//;
+                       $s =~ s/\$\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\)//g;
+                       $s =~ s/%%[^%]*%%//g;
+                       my @syms = split /[^a-zA-Z0-9_]+/, $s;
+                       map {
+                               $refs{$_}++ if (/[a-zA-Z]/ && $_ ne "if" && $_ ne "y" && $_ ne "n" && $_ ne "m" && !/^0[xX][0-9a-fA-F]+$/);
+                       } @syms
+               }
+       }
+
+       close F;
+}
+
+my %known_syms = ();
+if (defined $kconfig_sym_check_excludes) {
+       my $file = $kconfig_sym_check_excludes;
+       open(F, "<", $file) or die "Cannot open $file: $!";
+       while (<F>) {
+               chomp;
+               next if /^\s*$/;
+               next if /^\s*#/;
+               $known_syms{$1}++ if (/^\s*([a-zA-Z0-9_]+)\s*(#.*)?$/);
+       }
+}
+
+my $ret = 0;
+foreach my $k (sort keys %refs) {
+       next if (exists $configs{$k} || exists $known_syms{$k});
+
+       print "$k";
+       print " - warning: '$k' is probably not what you want; Kconfig tristate literals are always lowercase ('n', 'y', 'm')" if ($k eq "N" || $k eq "Y" || $k eq "M");
+       print "\n";
+
+       $ret = 1;
+}
+
+exit $ret;