[PATCH v3] kconfig: add kconfig-sym-check static checker

Andrew Jones posted 1 patch 2 days, 22 hours ago
Makefile                             |  23 +++--
scripts/kconfig/kconfig-sym-check.pl | 123 +++++++++++++++++++++++++++
2 files changed, 137 insertions(+), 9 deletions(-)
create mode 100755 scripts/kconfig/kconfig-sym-check.pl
[PATCH v3] kconfig: add kconfig-sym-check static checker
Posted by Andrew Jones 2 days, 22 hours ago
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 [1]
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>
---

v3:
  - Filter out scripts/kconfig/tests Kconfig files since they may
    be wrong on purpose and indeed there is a 'config Y' in there
    which would mask improper use of 'Y'. [Julian and Randy]
  - Fixed breakage introduced in v2 when attempting to be too
    clever...
  - More changes from another sashiko review which required the
    Perl to get even uglier. So ugly that I enlisted Claude to
    help generate it.
  - Added a sentence to the commit message to describe the excludes
    file format.

v2:
  - Added Andy's and Randy's tags
  - Accept srctree as first argument so the Makefile can drop 'cd $(srctree)' [Nathan]
  - Replace git ls-files with git+find fallback [Nathan and Andy]
  - Changes thanks to sashiko's review
    - strip quoted strings before inline comments to avoid '#' inside a string
    - use [^)]* instead of .* in macro strip regex to avoid greedy match
      eating tokens between adjacent $(macro) expansions


 Makefile                             |  23 +++--
 scripts/kconfig/kconfig-sym-check.pl | 123 +++++++++++++++++++++++++++
 2 files changed, 137 insertions(+), 9 deletions(-)
 create mode 100755 scripts/kconfig/kconfig-sym-check.pl

diff --git a/Makefile b/Makefile
index fbc67fcb6cdb..92c8ed867d0d 100644
--- 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 \
@@ -1805,14 +1806,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'
@@ -2232,7 +2234,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) \
@@ -2247,6 +2249,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
index 000000000000..3c3f989c05a7
--- /dev/null
+++ b/scripts/kconfig/kconfig-sym-check.pl
@@ -0,0 +1,123 @@
+#!/usr/bin/env perl
+# SPDX-License-Identifier: GPL-2.0
+
+use warnings;
+use strict;
+
+my $srctree = shift @ARGV;
+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;
-- 
2.43.0
Re: [PATCH v3] kconfig: add kconfig-sym-check static checker
Posted by Julian Braha 2 days, 22 hours ago
On 5/21/26 21:44, Andrew Jones wrote:
> The checker also warns about uppercase N/Y/M used as tristate literal
> values following the same logic as checkpatch.

Thanks, this is better! But...

While it does catch the Y case now, it still doesn't catch 'Y' with
quotes. Again, I had to manually inject this into Kconfig; I don't
actually know if this is used anywhere.

- Julian Braha
Re: [PATCH v3] kconfig: add kconfig-sym-check static checker
Posted by Andrew Jones 2 days, 21 hours ago
On Thu, May 21, 2026 at 10:01:37PM +0100, Julian Braha wrote:
> On 5/21/26 21:44, Andrew Jones wrote:
> > The checker also warns about uppercase N/Y/M used as tristate literal
> > values following the same logic as checkpatch.
> 
> Thanks, this is better! But...
> 
> While it does catch the Y case now, it still doesn't catch 'Y' with
> quotes. Again, I had to manually inject this into Kconfig; I don't
> actually know if this is used anywhere.
>

Ah, I see kconfig interprets "default 'y'" equivalently to "default y".
That's likely due to an implementation quirk of sym_lookup() and it's a
bit frustrating and maybe even deserves a fix. At least 'Y' is still
invalid, so we can look for "N", 'N', "M", 'M', "Y", 'Y' and complain
when we see them, but that seems more like a job for the corresponding
checkpatch patch[1]. This script is looking for symbols, and symbols
should never be quoted. We only add the extra warning text in this
script for dangling N, M, Y since there's a good chance those should
have been n, m, y instead.

[1] https://lore.kernel.org/all/20260521204605.534862-1-andrew.jones@linux.dev/

Thanks,
drew
Re: [PATCH v3] kconfig: add kconfig-sym-check static checker
Posted by Julian Braha 2 days, 19 hours ago
On 5/21/26 23:31, Andrew Jones wrote:
> This script is looking for symbols, and symbols
> should never be quoted.

Good point, out of scope.

It looks good to me otherwise, so:

Tested-by: Julian Braha <julianbraha@gmail.com>
Re: [PATCH v3] kconfig: add kconfig-sym-check static checker
Posted by Andrew Jones 2 days, 4 hours ago
On Fri, May 22, 2026 at 01:08:38AM +0100, Julian Braha wrote:
> On 5/21/26 23:31, Andrew Jones wrote:
> > This script is looking for symbols, and symbols
> > should never be quoted.
> 
> Good point, out of scope.
> 
> It looks good to me otherwise, so:
> 
> Tested-by: Julian Braha <julianbraha@gmail.com>

Thanks! I'll also send a v4 of this patch to resolve the final sashiko
comment since it looks worth doing.

drew