* [PATCH v4 0/5] add kconfirm
@ 2026-07-27 0:16 Julian Braha
2026-07-27 0:16 ` [PATCH v4 1/5] kconfig: add add another callback to the parser to view raw parse tree Julian Braha
` (5 more replies)
0 siblings, 6 replies; 10+ messages in thread
From: Julian Braha @ 2026-07-27 0:16 UTC (permalink / raw)
To: nathan, nsc
Cc: ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs, andrew.jones,
masahiroy, corbet, qingfang.deng, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
kconfirm now uses the in-tree parser. Making this migration required a
modification to the parser that allows us to observe the parse tree before
its final simplification step, thus allowing us to detect dead code.
Since no external crates are now necessary, I’ve removed the Cargo requirement,
too.
I believe these changes should resolve the major questions, so I’ve
removed the RFC tag.
Now, onto the existing patch-set description since the RFCs:
===
kconfirm is a tool to detect misusage of Kconfig. It detects dead code,
constant conditions, and invalid (reverse) ranges. There are also optional
checks to detect config options that select visible config options, and to
check for dead links in the help texts.
See also kconfirm's original introduction to the mailing list:
https://lore.kernel.org/all/6ec4df6d-1445-48ca-8f54-1d1a83c4716d@gmail.com/
False Alarms:
kconfirm aims for zero false-positives, though this is not completely
feasible due to macro evaluation from the host environment, primarily
affecting host compiler-related options. There will also be some false
positives for dead link checks, as this depends on an internet connection,
and we do not attempt to bypass bot blocks. For this reason, dead link
checking is disabled by default, but I've provided an example below of how
to enable it. Additionally, you can view my previous message to the
mailing list with hand-verified dead links here:
https://lore.kernel.org/all/6732bf08-41ee-40c4-83b2-4ae8bc0da7cf@gmail.com/
Additionally, there is an optional check to detect config options that
select visible config options, as requested by Jani during the review of
the first RFC:
https://lore.kernel.org/all/dcb7439832f0bb35598fba653d922b5f6a4d0058@intel.com/
Even after deduplicating across architectures, there are well over 1,000
instances of these select-visible cases, and I suspect that, despite the
Kconfig documentation saying select-visible should be avoided, some
exceptions will be made. So, I have left this check disabled by default,
keeping in line with the goal of having a low-noise checker. If interested
in using it, I have included an example below of how to enable this check.
Current State of Alarms:
With x86-64 on Linux v7.2-rc4 (which this RFC is based), there are 1282
alarms coming from the default set of checks, and an additional 976 alarms
if enabling the optional select-visible check. The last time I checked
linux-next (next-20260427), there were 81 unique dead links.
The most critical check is the dead default statements, which has surfaced
a few misconfiguration bugs (fortunately, just for kunit tests), see
examples:
https://lore.kernel.org/all/20260323124118.1414913-1-julianbraha@gmail.com/
and:
https://lore.kernel.org/all/20260323123536.1413732-1-julianbraha@gmail.com/
But hopefully kconfirm can ease maintenance and we can prevent more of
these from making it into the tree in the future.
Use it:
You can test out kconfirm with this patch series by compiling and running
kconfirm like this:
`make kconfirm`
To enable the select-visible check:
`KCONFIRM_ARGS="--enable-check select_visible" make kconfirm`
And to enable dead link checks in the help texts:
`KCONFIRM_ARGS="--enable-check dead_link" make kconfirm`
kconfirm by default runs on the same architecture as the kernel build
would. To run kconfirm on another architecture (for example, ARM with an
X86 host):
`ARCH=arm make kconfirm`
Thanks,
Julian Braha
---
Changes since RFC v3:
- Modify kconfig parser to make raw parse tree viewable to enable analysis
- Switch from external parser to in-tree kconfig parser (Demi)
- Add Rust bindings for kconfig
- Remove Cargo and external crates
- Make curl an optional dependency for optional dead link check (Arnd)
- Switch from libcurl to curl CLI for dead link checks (Miguel)
- Adhere to Rust-for-Linux style (Miguel)
- Add tests (Miguel)
- Move kconfirm under scripts/kconfig/ to resolve broken tab autocompletion (Nathan)
- Remove ungrouped attributes style check
- Add support for checking liveness of ftp and git URIs in help texts
- Dropped RFC tag
Link to RFC v3:
https://lore.kernel.org/all/20260516215354.449807-1-julianbraha@gmail.com/
Changes since RFC v2:
- Reduce Rust dependencies significantly (follows Demi's suggestions):
- from 6 direct dependencies to 1
- from 107 indirect dependencies to 4
- Replace ureq crate with usage of system libcurl (thanks Demi)
- Replace clap crate with FFI bindings to libc's getopt_long (also Demi)
- Remove crates env_logger, regex
- Switch from vendoring dependencies to requiring users to first download
outside of Make (as suggested by Miguel)
- Various makefile improvements (as pointed out by Nicolas):
- Fix out-of-tree builds
- Only delete kconfirm artifacts with 'distclean' and 'mrproper'
- Add myself as maintainer of kconfirm (as discussed with Nicolas)
- Remove dedicated code license file (pointed out by Jani)
- Update documentation to explain tool setup
- Add hint to users to check documentation and download tool dependencies
- Address sashiko's many code-level and documentation suggestions:
- Follow the kernel's rust import style
- Fix a dead_range/duplicate_range alarm mixup
- Fix potential duplicates in default value style check
- Avoid panicking on errors
- Clarify parse failure check usage in documentation
- Fix typo in documentation
- Can now enable architectures and disable the default (host) architecture in the CLI
Link to RFC v2:
https://lore.kernel.org/all/20260509203808.1142311-1-julianbraha@gmail.com/
Changes since RFC v1:
- vendored dependencies instead of requiring an internet connection
- removed Cargo.lock
- replaced reqwest dependency with smaller ureq
- removed rustls, expect user to have openssl instead
- added select-visible check based on Jani's feature request
- added invalid (reverse) range check
- deduplicating alarms that appear for multiple architectures
- `make clean` no longer deletes kconfirm's build artifacts
- typo fixes in documentation
- added patch description for the main "add kconfirm" patch (patch 1/2)
Link to RFC v1:
https://lore.kernel.org/all/20260427174429.779474-1-julianbraha@gmail.com/
---
Julian Braha (5):
kconfig: add add another callback to the parser to view raw parse tree
kconfig: add kconfirm
kconfirm: add tests
Documentation: add kconfirm
MAINTAINERS: add entry for kconfirm
Documentation/dev-tools/index.rst | 1 +
Documentation/dev-tools/kconfirm.rst | 229 ++++++
MAINTAINERS | 7 +
Makefile | 17 +-
scripts/kconfig/.gitignore | 1 +
scripts/kconfig/Makefile | 54 ++
scripts/kconfig/kconfig.rs | 445 +++++++++++
scripts/kconfig/kconfirm/.gitignore | 2 +
scripts/kconfig/kconfirm/analyze.rs | 340 ++++++++
scripts/kconfig/kconfirm/arch.rs | 53 ++
scripts/kconfig/kconfirm/checks.rs | 748 ++++++++++++++++++
scripts/kconfig/kconfirm/dead_links.rs | 230 ++++++
scripts/kconfig/kconfirm/kconfirm-cfg.sh | 57 ++
scripts/kconfig/kconfirm/kconfirm.rs | 278 +++++++
scripts/kconfig/kconfirm/output.rs | 87 ++
scripts/kconfig/kconfirm/symbol_table.rs | 105 +++
.../kconfig/kconfirm/tests/arch/arm/Kconfig | 9 +
.../kconfirm/tests/arch/powerpc/Kconfig | 4 +
.../kconfig/kconfirm/tests/arch/riscv/Kconfig | 9 +
.../kconfig/kconfirm/tests/arch/sh/Kconfig | 4 +
.../kconfirm/tests/arch/testarch/Kconfig | 4 +
.../kconfig/kconfirm/tests/arch/um/Kconfig | 4 +
.../kconfig/kconfirm/tests/arch/x86/Kconfig | 4 +
.../kconfirm/tests/architecture.Kconfig | 4 +
.../tests/architecture_common.Kconfig | 19 +
.../kconfirm/tests/conditional_prompt.Kconfig | 17 +
scripts/kconfig/kconfirm/tests/conftest.py | 93 +++
...nt_condition_negative_expression_1.Kconfig | 13 +
...nt_condition_negative_expression_2.Kconfig | 13 +
...nstant_condition_negative_symbol_1.Kconfig | 13 +
...nstant_condition_negative_symbol_2.Kconfig | 13 +
.../kconfig/kconfirm/tests/dead_link.Kconfig | 12 +
.../tests/default_categorization.Kconfig | 20 +
scripts/kconfig/kconfirm/tests/pytest.ini | 2 +
scripts/kconfig/kconfirm/tests/ranges.Kconfig | 39 +
.../kconfirm/tests/select_imply.Kconfig | 28 +
.../kconfig/kconfirm/tests/test_kconfirm.py | 358 +++++++++
scripts/kconfig/lkc_proto.h | 2 +
scripts/kconfig/parser.y | 21 +
39 files changed, 3357 insertions(+), 2 deletions(-)
create mode 100644 Documentation/dev-tools/kconfirm.rst
create mode 100644 scripts/kconfig/kconfig.rs
create mode 100644 scripts/kconfig/kconfirm/.gitignore
create mode 100644 scripts/kconfig/kconfirm/analyze.rs
create mode 100644 scripts/kconfig/kconfirm/arch.rs
create mode 100644 scripts/kconfig/kconfirm/checks.rs
create mode 100644 scripts/kconfig/kconfirm/dead_links.rs
create mode 100755 scripts/kconfig/kconfirm/kconfirm-cfg.sh
create mode 100644 scripts/kconfig/kconfirm/kconfirm.rs
create mode 100644 scripts/kconfig/kconfirm/output.rs
create mode 100644 scripts/kconfig/kconfirm/symbol_table.rs
create mode 100644 scripts/kconfig/kconfirm/tests/arch/arm/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/powerpc/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/riscv/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/sh/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/testarch/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/um/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/x86/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/architecture.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/architecture_common.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/conditional_prompt.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/conftest.py
create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_1.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_2.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_1.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_2.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/dead_link.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/default_categorization.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/pytest.ini
create mode 100644 scripts/kconfig/kconfirm/tests/ranges.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/select_imply.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/test_kconfirm.py
--
2.54.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v4 1/5] kconfig: add add another callback to the parser to view raw parse tree
2026-07-27 0:16 [PATCH v4 0/5] add kconfirm Julian Braha
@ 2026-07-27 0:16 ` Julian Braha
2026-07-27 0:16 ` [PATCH 2/5] kconfig: add kconfirm Julian Braha
` (4 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Julian Braha @ 2026-07-27 0:16 UTC (permalink / raw)
To: nathan, nsc
Cc: ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs, andrew.jones,
masahiroy, corbet, qingfang.deng, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
The final step of the Kconfig parser is to call menu_finalize(), which
will evaluate the dead code that static code analyzers like kconfirm need
to be able to view.
This patch adds another callback to the parser just before this call to
menu_finalize(), making the raw parse tree viewable as necessary to
callers of conf_set_pre_finalize_callback().
Assisted-by: Claude:claude-fable-5
Signed-off-by: Julian Braha <julianbraha@gmail.com>
---
scripts/kconfig/lkc_proto.h | 2 ++
scripts/kconfig/parser.y | 21 +++++++++++++++++++++
2 files changed, 23 insertions(+)
diff --git a/scripts/kconfig/lkc_proto.h b/scripts/kconfig/lkc_proto.h
index 8914b4e8f2a8..f03bcef7b270 100644
--- a/scripts/kconfig/lkc_proto.h
+++ b/scripts/kconfig/lkc_proto.h
@@ -6,6 +6,8 @@
/* confdata.c */
void conf_parse(const char *name);
+void conf_set_pre_finalize_callback(void (*fn)(const struct menu *root, void *data),
+ void *data);
int conf_read(const char *name);
int conf_read_simple(const char *name, int);
int conf_write_defconfig(const char *name);
diff --git a/scripts/kconfig/parser.y b/scripts/kconfig/parser.y
index 5fb6f07b6ad2..365ed450e855 100644
--- a/scripts/kconfig/parser.y
+++ b/scripts/kconfig/parser.y
@@ -28,6 +28,9 @@ static void zconf_error(const char *err, ...);
static bool zconf_endtoken(const char *tokenname,
const char *expected_tokenname);
+static void (*conf_pre_finalize_callback)(const struct menu *root, void *data);
+static void *conf_pre_finalize_callback_data;
+
struct menu *current_menu, *current_entry, *current_choice;
%}
@@ -551,6 +554,20 @@ static int choice_check_sanity(const struct menu *menu)
return ret;
}
+/*
+ * Register a callback to be invoked by conf_parse() once parsing has finished,
+ * but before menu_finalize() propagates dependencies and simplifies
+ * expressions. This hands the callback the menu tree as it was written in the
+ * Kconfig source files, which is what static analysis tools like kconfirm
+ * need. Passing NULL unregisters the callback.
+ */
+void conf_set_pre_finalize_callback(void (*fn)(const struct menu *root, void *data),
+ void *data)
+{
+ conf_pre_finalize_callback = fn;
+ conf_pre_finalize_callback_data = fn ? data : NULL;
+}
+
void conf_parse(const char *name)
{
struct menu *menu;
@@ -587,6 +604,10 @@ void conf_parse(const char *name)
menu_add_prompt(P_MENU, "Main menu", NULL);
}
+ if (conf_pre_finalize_callback)
+ conf_pre_finalize_callback(&rootmenu,
+ conf_pre_finalize_callback_data);
+
menu_finalize();
menu_for_each_entry(menu) {
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 2/5] kconfig: add kconfirm
2026-07-27 0:16 [PATCH v4 0/5] add kconfirm Julian Braha
2026-07-27 0:16 ` [PATCH v4 1/5] kconfig: add add another callback to the parser to view raw parse tree Julian Braha
@ 2026-07-27 0:16 ` Julian Braha
2026-07-27 0:16 ` [PATCH v4 3/5] kconfirm: add tests Julian Braha
` (3 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Julian Braha @ 2026-07-27 0:16 UTC (permalink / raw)
To: nathan, nsc
Cc: ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs, andrew.jones,
masahiroy, corbet, qingfang.deng, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
Add kconfirm into scripts/kconfig/
kconfirm is a static analysis tool with various checks for Kconfig, and
intended to have zero false alarms by default, though some are expected
for host compiler-related config options due to macro expansion. The
default checks currently include dead code, constant conditions, and
invalid (reverse) ranges.
There are also optional checks for dead links in the help texts, and for
config options that select visible config options.
kconfirm runs semantic analysis on the unsimplified parse tree that the
Kconfig parser provides. Config option definitions are collected into its
symbol table, and checks run from there using informed heuristics. The
dead link check uses the curl CLI to check the liveness of links, and is
optional.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Julian Braha <julianbraha@gmail.com>
---
Makefile | 17 +-
scripts/kconfig/.gitignore | 1 +
scripts/kconfig/Makefile | 54 ++
scripts/kconfig/kconfig.rs | 445 ++++++++++++++
scripts/kconfig/kconfirm/.gitignore | 2 +
scripts/kconfig/kconfirm/analyze.rs | 340 +++++++++++
scripts/kconfig/kconfirm/arch.rs | 53 ++
scripts/kconfig/kconfirm/checks.rs | 748 +++++++++++++++++++++++
scripts/kconfig/kconfirm/dead_links.rs | 230 +++++++
scripts/kconfig/kconfirm/kconfirm-cfg.sh | 57 ++
scripts/kconfig/kconfirm/kconfirm.rs | 278 +++++++++
scripts/kconfig/kconfirm/output.rs | 87 +++
scripts/kconfig/kconfirm/symbol_table.rs | 105 ++++
13 files changed, 2415 insertions(+), 2 deletions(-)
create mode 100644 scripts/kconfig/kconfig.rs
create mode 100644 scripts/kconfig/kconfirm/.gitignore
create mode 100644 scripts/kconfig/kconfirm/analyze.rs
create mode 100644 scripts/kconfig/kconfirm/arch.rs
create mode 100644 scripts/kconfig/kconfirm/checks.rs
create mode 100644 scripts/kconfig/kconfirm/dead_links.rs
create mode 100755 scripts/kconfig/kconfirm/kconfirm-cfg.sh
create mode 100644 scripts/kconfig/kconfirm/kconfirm.rs
create mode 100644 scripts/kconfig/kconfirm/output.rs
create mode 100644 scripts/kconfig/kconfirm/symbol_table.rs
diff --git a/Makefile b/Makefile
index b568bfe8f3ec..e036af320486 100644
--- a/Makefile
+++ b/Makefile
@@ -298,7 +298,7 @@ no-dot-config-targets := $(clean-targets) \
%asm-generic kernelversion %src-pkg dt_binding_check \
dt_style_selftest \
outputmakefile rustavailable rustfmt rustfmtcheck \
- run-command
+ run-command kconfirm kconfirmtest
no-sync-config-targets := $(no-dot-config-targets) %install modules_sign kernelrelease \
image_name
single-targets := %.a %.i %.ko %.lds %.ll %.lst %.mod %.o %.rsi %.s %/
@@ -1879,6 +1879,7 @@ help:
@echo ' headerdep - Detect inclusion cycles in headers'
@echo ' coccicheck - Check with Coccinelle'
@echo ' kconfig-sym-check - Check for dangling Kconfig symbol references'
+ @echo ' kconfirm - Run static analysis on the Kconfig tree'
@echo ' clang-analyzer - Check with clang static analyzer'
@echo ' clang-tidy - Check with clang-tidy'
@echo ''
@@ -2328,7 +2329,7 @@ endif
# Scripts to check various things for consistency
# ---------------------------------------------------------------------------
-PHONY += includecheck versioncheck coccicheck kconfig-sym-check
+PHONY += includecheck versioncheck coccicheck kconfig-sym-check kconfirm kconfirmtest
includecheck:
find $(srctree)/* $(RCS_FIND_IGNORE) \
@@ -2346,6 +2347,18 @@ coccicheck:
kconfig-sym-check:
$(Q)$(PERL) $(srctree)/scripts/kconfig/kconfig-sym-check.pl $(srctree) $(KCONFIG_SYM_CHECK_EXCLUDES)
+kconfirm-hostrustc = $(if $(filter 1,$(KBUILD_CLIPPY)),HOSTRUSTC=$(CLIPPY_DRIVER))
+
+kconfirm: export CC_VERSION_TEXT := $(CC_VERSION_TEXT)
+kconfirm: export RUSTC_VERSION_TEXT := $(RUSTC_VERSION_TEXT)
+kconfirm: export PAHOLE_VERSION := $(PAHOLE_VERSION)
+kconfirm: outputmakefile scripts_basic
+ $(Q)$(MAKE) $(build)=scripts/kconfig $(kconfirm-hostrustc) kconfirm || \
+ (printf "\n kconfirm failed to build or run. It is built with the Rust\n toolchain (rustc, bindgen). See Documentation/dev-tools/kconfirm.rst\n\n" && false)
+
+kconfirmtest: outputmakefile scripts_basic
+ $(Q)$(MAKE) $(build)=scripts/kconfig $(kconfirm-hostrustc) kconfirmtest
+
PHONY += checkstack kernelrelease kernelversion image_name
# UML needs a little special treatment here. It wants to use the host
diff --git a/scripts/kconfig/.gitignore b/scripts/kconfig/.gitignore
index 0b2ff775b2e3..0c2f9763029c 100644
--- a/scripts/kconfig/.gitignore
+++ b/scripts/kconfig/.gitignore
@@ -4,4 +4,5 @@
/[gmnq]conf-bin
/[gmnq]conf-cflags
/[gmnq]conf-libs
+/kconfig_bindings.rs
/qconf-moc.cc
diff --git a/scripts/kconfig/Makefile b/scripts/kconfig/Makefile
index 5baf1c44ffa2..ffdf36a21d79 100644
--- a/scripts/kconfig/Makefile
+++ b/scripts/kconfig/Makefile
@@ -154,6 +154,7 @@ help:
@echo ' default value without prompting'
@echo ' tinyconfig - Configure the tiniest possible kernel'
@echo ' testconfig - Run Kconfig unit tests (requires python3 and pytest)'
+ @echo ' kconfirmtest - Run kconfirm tests (requires python3 and pytest)'
@echo ''
@echo 'Configuration topic targets:'
@$(foreach f, $(all-config-fragments), \
@@ -234,3 +235,56 @@ $(obj)/%conf-cflags $(obj)/%conf-libs $(obj)/%conf-bin: $(src)/%conf-cfg.sh
$(call cmd,conf_cfg)
clean-files += *conf-cflags *conf-libs *conf-bin
+
+# kconfirm: analyzes Kconfig using the un-simplified parse tree.
+hostprogs += kconfirm/kconfirm
+kconfirm/kconfirm-rust := y
+kconfirm/kconfirm-objs := $(common-objs)
+targets += kconfig_bindings.rs
+
+HOSTRUSTFLAGS_kconfirm/kconfirm := \
+ $(addprefix -Clink-arg=$(obj)/,$(kconfirm/kconfirm-objs))
+KCONFIG_BINDINGS := $(abspath $(obj)/kconfig_bindings.rs)
+export KCONFIG_BINDINGS
+
+# The Rust bindings are generated from the parser's C headers.
+quiet_cmd_kconfig_bindgen = BINDGEN $@
+ cmd_kconfig_bindgen = \
+ $(BINDGEN) $< --rust-target 1.85 \
+ --no-doc-comments --no-prepend-enum-name \
+ --allowlist-function 'conf_parse' \
+ --allowlist-function 'conf_set_pre_finalize_callback' \
+ --allowlist-function 'expr_print' \
+ --allowlist-type 'expr' --allowlist-type 'menu' \
+ --allowlist-type 'property' --allowlist-type 'symbol' \
+ --allowlist-type 'expr_type' --allowlist-type 'menu_type' \
+ --allowlist-type 'prop_type' --allowlist-type 'symbol_type' \
+ -o $@ -- -I $(src) -I $(srctree)/scripts/include
+
+$(obj)/kconfig_bindings.rs: $(src)/lkc.h $(src)/expr.h $(src)/lkc_proto.h \
+ $(srctree)/scripts/include/list_types.h FORCE
+ $(call if_changed,kconfig_bindgen)
+
+$(obj)/kconfirm/kconfirm: $(obj)/kconfig_bindings.rs \
+ $(addprefix $(obj)/,$(kconfirm/kconfirm-objs))
+
+# Alert the user when the Rust toolchain is missing or too old.
+PHONY += kconfirm-tool-check
+kconfirm-tool-check:
+ $(Q)$(CONFIG_SHELL) $(src)/kconfirm/kconfirm-cfg.sh
+
+$(obj)/kconfig_bindings.rs: | kconfirm-tool-check
+$(obj)/kconfirm/kconfirm: | kconfirm-tool-check
+
+PHONY += kconfirm
+kconfirm: $(obj)/kconfirm/kconfirm
+ $(Q)$< --linux-path $(abspath $(srctree)) --kconfig $(Kconfig) \
+ $(KCONFIRM_ARGS)
+
+PHONY += kconfirmtest
+kconfirmtest: $(obj)/kconfirm/kconfirm
+ $(Q)$(PYTHON3) -B -m pytest $(src)/kconfirm/tests \
+ --kconfirm $(abspath $<) \
+ -o cache_dir=$(abspath $(obj)/kconfirm/tests/.cache) \
+ $(if $(findstring 1,$(KBUILD_VERBOSE)),--capture=no)
+clean-files += kconfirm/tests/.cache
diff --git a/scripts/kconfig/kconfig.rs b/scripts/kconfig/kconfig.rs
new file mode 100644
index 000000000000..2b27f3c83a51
--- /dev/null
+++ b/scripts/kconfig/kconfig.rs
@@ -0,0 +1,445 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Julian Braha <julianbraha@gmail.com>
+ */
+//! Rust abstraction over the Kconfig parser and its bindings. Only covers
+//! the unsimplified parse tree for now; useful for static analyzers, such as
+//! kconfirm.
+//!
+//! The `raw` module below is generated by bindgen from the parser's C headers.
+use std::{
+ ffi::CStr,
+ fmt,
+ os::raw::{
+ c_char,
+ c_int,
+ c_void, //
+ },
+ panic::{
+ catch_unwind,
+ AssertUnwindSafe, //
+ },
+ ptr, //
+};
+
+#[allow(
+ clippy::all,
+ dead_code,
+ non_camel_case_types,
+ non_upper_case_globals,
+ unreachable_pub
+)]
+mod raw {
+ include!(env!("KCONFIG_BINDINGS"));
+}
+
+use raw::*;
+
+type Expr = expr;
+type Menu = menu;
+type Symbol = symbol;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub(crate) struct Expression {
+ rendered: String,
+ negation: String,
+}
+
+impl Expression {
+ pub(crate) fn is_negation_of(&self, other: &Self) -> bool {
+ self.negation == other.rendered
+ }
+
+ fn constant(value: &str, negation: &str) -> Self {
+ Self {
+ rendered: value.to_string(),
+ negation: negation.to_string(),
+ }
+ }
+}
+
+impl fmt::Display for Expression {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.rendered.fmt(formatter)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct DefaultAttribute {
+ pub(crate) expression: Expression,
+ pub(crate) r#if: Option<Expression>,
+}
+
+impl fmt::Display for DefaultAttribute {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.expression.fmt(formatter)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct Select {
+ pub(crate) symbol: String,
+ pub(crate) r#if: Option<Expression>,
+}
+
+impl fmt::Display for Select {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.symbol.fmt(formatter)
+ }
+}
+
+pub(crate) type Imply = Select;
+
+#[derive(Clone, Debug)]
+pub(crate) struct RangeBound {
+ pub(crate) value: String,
+}
+
+impl fmt::Display for RangeBound {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.value.fmt(formatter)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct Range {
+ pub(crate) lower_bound: RangeBound,
+ pub(crate) upper_bound: RangeBound,
+ pub(crate) r#if: Option<Expression>,
+}
+
+impl fmt::Display for Range {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(formatter, "{} {}", self.lower_bound, self.upper_bound)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct Prompt {
+ pub(crate) r#if: Option<Expression>,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) enum Attribute {
+ Default(DefaultAttribute),
+ DependsOn(Expression),
+ Select(Select),
+ Imply(Imply),
+ Range(Range),
+ Help(String),
+ Prompt(Prompt),
+}
+
+#[derive(Debug)]
+pub(crate) struct Config {
+ pub(crate) symbol: String,
+ pub(crate) attributes: Vec<Attribute>,
+}
+
+#[derive(Debug)]
+pub(crate) struct KconfigMenu {
+ pub(crate) depends_on: Vec<Expression>,
+ pub(crate) entries: Vec<Entry>,
+}
+
+#[derive(Debug)]
+pub(crate) struct Choice {
+ pub(crate) options: Vec<Attribute>,
+ pub(crate) entries: Vec<Entry>,
+}
+
+#[derive(Debug)]
+pub(crate) struct If {
+ pub(crate) condition: Expression,
+ pub(crate) entries: Vec<Entry>,
+}
+
+#[derive(Debug)]
+pub(crate) enum Entry {
+ Config(Config),
+ Menu(KconfigMenu),
+ Choice(Choice),
+ If(If),
+ Comment,
+}
+
+#[expect(
+ clippy::disallowed_methods,
+ reason = "host tools use the platform c_char ABI, not the kernel's unsigned c_char"
+)]
+fn string(pointer: *const c_char) -> Option<String> {
+ if pointer.is_null() {
+ None
+ } else {
+ // SAFETY: All strings in the parse tree are NUL-terminated.
+ let string = unsafe { CStr::from_ptr(pointer) };
+ Some(string.to_string_lossy().into_owned())
+ }
+}
+
+fn symbol_name(symbol: *const Symbol) -> String {
+ // SAFETY: Callers only pass non-null symbols from the parse tree.
+ string(unsafe { (*symbol).name }).unwrap_or_default()
+}
+
+/// Appends one `expr_print()` fragment to the `String` that backs `data`.
+///
+/// # Safety
+///
+/// `data` must be the `String` pointer supplied by [`render()`], and `text`
+/// must be NUL-terminated or null.
+unsafe extern "C" fn append_expr_text(
+ data: *mut c_void,
+ _symbol: *mut Symbol,
+ text: *const c_char,
+) {
+ // SAFETY: `render()` passes a `String` that outlives the `expr_print()`
+ // call.
+ let output = unsafe { &mut *data.cast::<String>() };
+ if let Some(text) = string(text) {
+ output.push_str(&text);
+ }
+}
+
+/// Renders `expr` the way the frontends print it.
+fn render(expr: *const Expr, prevtoken: expr_type) -> String {
+ let mut output = String::new();
+ // SAFETY: `expr` points into the parse tree, the callback is
+ // synchronous, and `output` outlives the call.
+ unsafe {
+ expr_print(
+ expr,
+ Some(append_expr_text),
+ ptr::from_mut(&mut output).cast(),
+ prevtoken as c_int,
+ );
+ }
+ output
+}
+
+fn expression(expr: *const Expr) -> Option<Expression> {
+ if expr.is_null() {
+ return None;
+ }
+
+ let rendered = render(expr, E_NONE);
+
+ // Render the negation of `expr` through the same printer, so that it
+ // can be compared with another rendered expression.
+ // SAFETY: `expr` is non-null, and `E_NOT` nodes keep their operand in
+ // `left.expr`.
+ let negation = if unsafe { (*expr).type_ } == E_NOT {
+ // SAFETY: `expr` is non-null, and `E_NOT` nodes keep their operand in
+ // `left.expr`.
+ render(unsafe { (*expr).left.expr }, E_NONE)
+ } else {
+ format!("!{}", render(expr, E_NOT))
+ };
+
+ Some(Expression { rendered, negation })
+}
+
+fn dependencies(expr: *const Expr, output: &mut Vec<Expression>) {
+ if expr.is_null() {
+ return;
+ }
+
+ // SAFETY: `expr` points into the parse tree.
+ if unsafe { (*expr).type_ } == E_AND {
+ // SAFETY: `E_AND` nodes have an expression on both sides.
+ let (left, right) = unsafe { ((*expr).left.expr, (*expr).right.expr) };
+ dependencies(left, output);
+ dependencies(right, output);
+ } else if let Some(expr) = expression(expr) {
+ output.push(expr);
+ }
+}
+
+fn range_bound(symbol: *const Symbol) -> RangeBound {
+ RangeBound {
+ // SAFETY: Range expressions contain a symbol in each operand.
+ value: symbol_name(symbol),
+ }
+}
+
+fn attributes(menu: *const Menu, symbol: *const Symbol) -> Vec<Attribute> {
+ let mut attributes = Vec::new();
+
+ let mut menu_dependencies = Vec::new();
+ // SAFETY: `menu` points into the parse tree.
+ dependencies(unsafe { (*menu).dep }, &mut menu_dependencies);
+ attributes.extend(menu_dependencies.into_iter().map(Attribute::DependsOn));
+
+ // SAFETY: `symbol` points into the parse tree.
+ let mut property = unsafe { (*symbol).prop };
+ while !property.is_null() {
+ // A property belongs to the menu node of the location that defined it.
+ // SAFETY: Property links are valid during the callback.
+ if ptr::eq(unsafe { (*property).menu }, menu) {
+ // SAFETY: Property expression pointers remain during the
+ // callback.
+ let (value, condition) = unsafe { ((*property).expr, (*property).visible.expr) };
+ let condition = expression(condition);
+ // SAFETY: `property` points into the parse tree.
+ let property_type = unsafe { (*property).type_ };
+ match property_type {
+ // The parser rewrites a `menuconfig` prompt to `P_MENU`. Menus
+ // and the mainmenu have no symbol for properties, so their
+ // `P_MENU` never ends up in a property list.
+ P_PROMPT | P_MENU => {
+ attributes.push(Attribute::Prompt(Prompt { r#if: condition }));
+ }
+ P_DEFAULT => {
+ if let Some(expression) = expression(value) {
+ attributes.push(Attribute::Default(DefaultAttribute {
+ expression,
+ r#if: condition,
+ }));
+ }
+ }
+ P_SELECT | P_IMPLY => {
+ // SAFETY: Select and imply values are `E_SYMBOL`
+ // expressions, which keep their symbol in `left.sym`.
+ let target = unsafe { (*value).left.sym };
+ let select = Select {
+ // SAFETY: `target` is a non-null symbol.
+ symbol: symbol_name(target),
+ r#if: condition,
+ };
+ if property_type == P_SELECT {
+ attributes.push(Attribute::Select(select));
+ } else {
+ attributes.push(Attribute::Imply(select));
+ }
+ }
+ P_RANGE => {
+ // SAFETY: Range values are `E_RANGE` expressions, which
+ // keep a symbol in each operand.
+ let (lower_bound, upper_bound) =
+ unsafe { ((*value).left.sym, (*value).right.sym) };
+ attributes.push(Attribute::Range(Range {
+ lower_bound: range_bound(lower_bound),
+ upper_bound: range_bound(upper_bound),
+ r#if: condition,
+ }));
+ }
+ _ => {}
+ }
+ }
+ // SAFETY: `property` points into the property list.
+ property = unsafe { (*property).next };
+ }
+
+ // SAFETY: The help pointer is parser-owned and remains valid during the
+ // callback.
+ let help = unsafe { (*menu).help };
+ if let Some(help) = string(help) {
+ attributes.push(Attribute::Help(help));
+ }
+
+ attributes
+}
+
+fn menu_dependencies(menu: *const Menu) -> Vec<Expression> {
+ let mut output = Vec::new();
+ // SAFETY: `menu` points into the parse tree.
+ dependencies(unsafe { (*menu).dep }, &mut output);
+ output
+}
+
+fn entries(parent: *const Menu) -> Vec<Entry> {
+ let mut output = Vec::new();
+ // SAFETY: `parent` points into the parse tree.
+ let mut menu: *const Menu = unsafe { (*parent).list };
+
+ while !menu.is_null() {
+ // SAFETY: Menu links and fields remain valid during the callback.
+ let symbol: *const Symbol = unsafe { (*menu).sym };
+ // SAFETY: `menu` points into the parse tree.
+ let menu_type = unsafe { (*menu).type_ };
+ let entry = match menu_type {
+ // The parser records `config` and `menuconfig` entries as a menu
+ // node carrying a symbol. Neither opens a submenu while parsing;
+ // `menu_finalize()` is what nests later entries underneath them,
+ // and it has not run yet.
+ M_NORMAL | M_MENU if !symbol.is_null() => Entry::Config(Config {
+ symbol: symbol_name(symbol),
+ attributes: attributes(menu, symbol),
+ }),
+ M_NORMAL => panic!("Kconfig 'config' entry without a symbol"),
+ M_MENU => Entry::Menu(KconfigMenu {
+ depends_on: menu_dependencies(menu),
+ entries: entries(menu),
+ }),
+ // A choice is recorded as a symbol with no name, so its `options`
+ // are read the same way a config's attributes are.
+ M_CHOICE => Entry::Choice(Choice {
+ options: attributes(menu, symbol),
+ entries: entries(menu),
+ }),
+ M_IF => Entry::If(If {
+ // SAFETY: `If` entries always have a dependency expression.
+ condition: expression(unsafe { (*menu).dep })
+ .unwrap_or_else(|| Expression::constant("y", "n")),
+ entries: entries(menu),
+ }),
+ M_COMMENT => Entry::Comment,
+ value => panic!("unknown Kconfig menu type {value}"),
+ };
+ output.push(entry);
+
+ // SAFETY: `menu` points into the sibling list.
+ menu = unsafe { (*menu).next };
+ }
+
+ output
+}
+
+struct ParseState {
+ entries: Option<Vec<Entry>>,
+ panicked: bool,
+}
+
+/// Collects the parse tree before the parser finalizes (simplifies) it.
+///
+/// # Safety
+///
+/// `data` must point to the [`ParseState`] supplied by [`parse_kconfig()`].
+unsafe extern "C" fn collect_entries(root: *const Menu, data: *mut c_void) {
+ // SAFETY: `parse_kconfig()` passes a non-null `ParseState` that outlives
+ // the synchronous `conf_parse()` call.
+ let state = unsafe { &mut *data.cast::<ParseState>() };
+ match catch_unwind(AssertUnwindSafe(|| entries(root))) {
+ Ok(entries) => state.entries = Some(entries),
+ Err(_) => state.panicked = true,
+ }
+}
+
+/// Parses the Kconfig tree rooted at `name` relative to the current directory.
+///
+/// # Panics
+///
+/// Panics if the parser does not invoke the traversal callback or if
+/// traversing its parse tree encounters an unsupported node.
+#[expect(
+ clippy::disallowed_methods,
+ reason = "host tools use the platform c_char ABI, not the kernel's unsigned c_char"
+)]
+pub(crate) fn parse_kconfig(name: &CStr) -> Vec<Entry> {
+ let mut state = ParseState {
+ entries: None,
+ panicked: false,
+ };
+
+ // SAFETY: `conf_parse()` is synchronous, so `state` outlives the callback
+ // and its user-data pointer.
+ unsafe {
+ conf_set_pre_finalize_callback(Some(collect_entries), ptr::from_mut(&mut state).cast());
+ conf_parse(name.as_ptr());
+ conf_set_pre_finalize_callback(None, ptr::null_mut());
+ }
+ assert!(!state.panicked, "failed to traverse Kconfig graph");
+ state
+ .entries
+ .expect("Kconfig parser calls the traversal callback")
+}
diff --git a/scripts/kconfig/kconfirm/.gitignore b/scripts/kconfig/kconfirm/.gitignore
new file mode 100644
index 000000000000..f21a4d460aee
--- /dev/null
+++ b/scripts/kconfig/kconfirm/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+/kconfirm
diff --git a/scripts/kconfig/kconfirm/analyze.rs b/scripts/kconfig/kconfirm/analyze.rs
new file mode 100644
index 000000000000..192669405a88
--- /dev/null
+++ b/scripts/kconfig/kconfirm/analyze.rs
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Julian Braha <julianbraha@gmail.com>
+ */
+use crate::{
+ dead_links::{
+ self,
+ check_link,
+ LinkStatus, //
+ },
+ kconfig::{
+ Attribute::*,
+ Choice,
+ Config,
+ Entry,
+ Expression,
+ If,
+ Imply,
+ KconfigMenu,
+ Select, //
+ },
+ output::{
+ Finding,
+ Severity, //
+ },
+ symbol_table::{
+ AttributeDef,
+ SymbolUpdate, //
+ },
+ AnalysisArgs,
+ Check,
+ SymbolTable, //
+};
+use std::{
+ collections::HashSet, //
+ option::Option, //
+};
+
+fn check_text(
+ unique_links: &mut HashSet<String>,
+ text: &str,
+ args: &AnalysisArgs,
+ findings: &mut Vec<Finding>,
+ symbol: Option<&str>,
+ arch: &str,
+ context: &str,
+) {
+ if !args.is_enabled(Check::DeadLink) {
+ return;
+ }
+
+ for link in dead_links::find_links(text) {
+ // Avoid checking the same link more than once.
+ if !unique_links.insert(link.clone()) {
+ continue;
+ }
+
+ let status = check_link(&link);
+ if status != LinkStatus::Ok && status != LinkStatus::ProbablyBlocked {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadLink,
+ symbol: symbol.map(|s| s.to_string()),
+ message: format!(
+ "{} contains link {} with status {}",
+ context,
+ link,
+ status.as_str()
+ ),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct Context {
+ pub(crate) arch: String,
+ pub(crate) definition_condition: Vec<Expression>,
+ pub(crate) visibility: Vec<Option<Expression>>,
+ pub(crate) dependencies: Vec<Expression>,
+}
+
+impl Context {
+ fn with_arch(arch: &str) -> Context {
+ Context {
+ arch: arch.to_owned(),
+ definition_condition: vec![],
+ visibility: vec![],
+ dependencies: vec![],
+ }
+ }
+
+ fn child(&self) -> Self {
+ self.clone()
+ }
+
+ fn with_dep(mut self, dep: Expression) -> Self {
+ self.dependencies.push(dep);
+ self
+ }
+
+ fn with_visibility(mut self, cond: Option<Expression>) -> Self {
+ self.visibility.push(cond);
+ self
+ }
+
+ fn with_definition(mut self, cond: Expression) -> Self {
+ self.definition_condition.push(cond);
+ self
+ }
+}
+
+fn recurse_entries(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ unique_links: &mut HashSet<String>,
+ entries: Vec<Entry>,
+ ctx: Context,
+ findings: &mut Vec<Finding>,
+) {
+ for entry in entries {
+ process_entry(args, symtab, unique_links, entry, ctx.clone(), findings);
+ }
+}
+
+/// Traverses parsed Kconfig entries, populates `symtab`, and returns findings.
+pub(crate) fn analyze(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ arch: &str,
+ entries: Vec<Entry>,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let mut unique_links = HashSet::new();
+
+ let ctx = Context::with_arch(arch);
+
+ recurse_entries(args, symtab, &mut unique_links, entries, ctx, &mut findings);
+
+ findings
+}
+
+// Config here refers to a config option in the parse tree.
+fn handle_config(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: Config,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+ unique_links: &mut HashSet<String>,
+) {
+ let config_symbol = entry.symbol;
+
+ let mut child_ctx = ctx.child();
+
+ let mut dependencies = Vec::new();
+ let mut kconfig_selects: Vec<Select> = Vec::new();
+ let mut kconfig_implies: Vec<Imply> = Vec::new();
+ let mut ranges = Vec::new();
+ let mut defaults = Vec::new();
+ let mut found_prompt = false;
+
+ for attribute in entry.attributes {
+ match attribute {
+ Default(default) => defaults.push(default),
+ DependsOn(depends_on) => dependencies.push(depends_on),
+ Select(select) => kconfig_selects.push(select),
+ Imply(imply) => kconfig_implies.push(imply),
+ // Kconfig range bounds are inclusive.
+ Range(range) => ranges.push(range),
+ Help(h) => {
+ check_text(
+ unique_links,
+ &h,
+ args,
+ findings,
+ Some(&config_symbol),
+ &ctx.arch,
+ "help text",
+ );
+ }
+
+ // A prompt's `if` expression determines its visibility.
+ Prompt(prompt) => {
+ found_prompt = true;
+ if let Some(c) = prompt.r#if {
+ child_ctx = child_ctx.with_visibility(Some(c));
+ }
+ }
+ }
+ }
+
+ if !found_prompt {
+ child_ctx = child_ctx.with_visibility(None);
+ }
+
+ // Add inherited dependencies to this symbol's own dependencies.
+ dependencies.extend(child_ctx.dependencies.clone());
+ symtab.merge_insert(SymbolUpdate {
+ symbol: config_symbol.clone(),
+ is_definition: true,
+ attributes: AttributeDef {
+ dependencies,
+ ranges,
+ defaults,
+ visibility: child_ctx.visibility.clone(),
+ selects: kconfig_selects
+ .clone()
+ .into_iter()
+ .map(|select| (select.symbol, select.r#if))
+ .collect(),
+ implies: kconfig_implies
+ .into_iter()
+ .map(|imply| (imply.symbol, imply.r#if))
+ .collect(),
+ },
+ definition_condition: child_ctx.definition_condition.clone(),
+ selected_by: None,
+ });
+
+ for select in kconfig_selects {
+ symtab.merge_insert(SymbolUpdate {
+ symbol: select.symbol,
+ is_definition: false,
+ attributes: AttributeDef::default(),
+ definition_condition: child_ctx.definition_condition.clone(),
+ selected_by: Some((config_symbol.clone(), select.r#if)),
+ });
+ }
+}
+
+fn handle_menu(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: KconfigMenu,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+ unique_links: &mut HashSet<String>,
+) {
+ let mut child_ctx = ctx.child();
+
+ for dep in entry.depends_on {
+ child_ctx = child_ctx.with_dep(dep.clone());
+ // Menu dependencies also constrain the visibility of their contained
+ // entries.
+ child_ctx = child_ctx.with_visibility(Some(dep));
+ }
+
+ let nested_entries = entry.entries;
+
+ recurse_entries(
+ args,
+ symtab,
+ unique_links,
+ nested_entries,
+ child_ctx.clone(),
+ findings,
+ );
+}
+
+fn handle_choice(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: Choice,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+ unique_links: &mut HashSet<String>,
+) {
+ let mut child_ctx = ctx.child();
+
+ // Choice members inherit both outer and choice-specific dependencies.
+ for attribute in entry.options {
+ match attribute {
+ DependsOn(depends_on) => {
+ child_ctx = child_ctx.with_dep(depends_on);
+ }
+
+ // A prompt's `if` expression determines its visibility.
+ Prompt(prompt) => {
+ if let Some(i) = prompt.r#if {
+ child_ctx = child_ctx.with_visibility(Some(i));
+ }
+ }
+ _ => {}
+ }
+ }
+
+ let nested_entries = entry.entries;
+
+ recurse_entries(
+ args,
+ symtab,
+ unique_links,
+ nested_entries,
+ child_ctx.clone(),
+ findings,
+ );
+}
+
+fn handle_if(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: If,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+ unique_links: &mut HashSet<String>,
+) {
+ let mut child_ctx = ctx.child();
+ child_ctx = child_ctx.with_definition(entry.condition.clone());
+ child_ctx = child_ctx.with_dep(entry.condition);
+ let nested_entries = entry.entries;
+
+ recurse_entries(
+ args,
+ symtab,
+ unique_links,
+ nested_entries,
+ child_ctx,
+ findings,
+ );
+}
+
+fn process_entry(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ unique_links: &mut HashSet<String>,
+ entry: Entry,
+ ctx: Context,
+ findings: &mut Vec<Finding>,
+) {
+ // Each handler updates the context for the construct it processes.
+ match entry {
+ Entry::Config(c) => handle_config(args, symtab, c, &ctx, findings, unique_links),
+ Entry::Menu(m) => handle_menu(args, symtab, m, &ctx, findings, unique_links),
+ Entry::Choice(c) => handle_choice(args, symtab, c, &ctx, findings, unique_links),
+ Entry::If(i) => handle_if(args, symtab, i, &ctx, findings, unique_links),
+ Entry::Comment => {}
+ }
+}
diff --git a/scripts/kconfig/kconfirm/arch.rs b/scripts/kconfig/kconfirm/arch.rs
new file mode 100644
index 000000000000..9af24771a341
--- /dev/null
+++ b/scripts/kconfig/kconfirm/arch.rs
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Julian Braha <julianbraha@gmail.com>
+ */
+use std::{
+ fs,
+ io,
+ path::Path, //
+};
+
+/// Returns the architecture directory names in a Linux source tree.
+pub(crate) fn available_architectures(linux_path: &Path) -> io::Result<Vec<String>> {
+ let mut architectures = Vec::new();
+
+ for entry in fs::read_dir(linux_path.join("arch"))? {
+ let entry = entry?;
+ if !entry.file_type()?.is_dir() {
+ continue;
+ }
+
+ let architecture = entry.file_name().into_string().map_err(|name| {
+ io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!("architecture directory name is not valid UTF-8: {name:?}"),
+ )
+ })?;
+ architectures.push(architecture);
+ }
+
+ architectures.sort();
+ Ok(architectures)
+}
+
+/// Maps the build architecture to its source directory, like the top-level
+/// Makefile's `SRCARCH`.
+pub(crate) fn arch_to_srcarch(arch: &str) -> &str {
+ match arch {
+ "i386" | "x86_64" => "x86",
+ "sparc32" | "sparc64" => "sparc",
+ "parisc64" => "parisc",
+ _ => arch,
+ }
+}
+
+/// Maps an architecture source directory to its Kconfig option name.
+pub(crate) fn arch_dir_to_config(arch_dir: &str) -> String {
+ match arch_dir {
+ "powerpc" => String::from("PPC"),
+ "sh" => String::from("SUPERH"),
+ "um" => String::from("UML"),
+ _ => arch_dir.to_uppercase(),
+ }
+}
diff --git a/scripts/kconfig/kconfirm/checks.rs b/scripts/kconfig/kconfirm/checks.rs
new file mode 100644
index 000000000000..c810db342fb7
--- /dev/null
+++ b/scripts/kconfig/kconfirm/checks.rs
@@ -0,0 +1,748 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Julian Braha <julianbraha@gmail.com>
+ */
+use crate::{
+ kconfig::{
+ Expression,
+ RangeBound, //
+ },
+ output::{
+ Finding,
+ Severity, //
+ },
+ symbol_table::{
+ AttributeDef,
+ SymbolInfo, //
+ },
+};
+use std::{
+ collections::HashSet,
+ num::ParseIntError,
+ str::FromStr, //
+};
+
+/// A diagnostic check that can be enabled or disabled from the command line.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub(crate) enum Check {
+ DeadLink,
+ SelectVisible,
+ DuplicateDependency,
+ DuplicateRange,
+ DeadRange,
+ DuplicateSelect,
+ DeadSelect,
+ DeadDefault,
+ ConstantCondition,
+ DuplicateDefault,
+ DuplicateDefaultValue,
+ DuplicateImply,
+ DeadImply,
+ ReverseRange,
+}
+
+impl Check {
+ /// Returns the stable command-line and diagnostic identifier for a check.
+ pub(crate) fn as_str(self) -> &'static str {
+ match self {
+ Check::DeadLink => "dead_link",
+ Check::SelectVisible => "select_visible",
+ Check::DuplicateDependency => "duplicate_dependency",
+ Check::DuplicateRange => "duplicate_range",
+ Check::DeadRange => "dead_range",
+ Check::DuplicateSelect => "duplicate_select",
+ Check::DeadSelect => "dead_select",
+ Check::DeadDefault => "dead_default",
+ Check::ConstantCondition => "constant_condition",
+ Check::DuplicateDefault => "duplicate_default",
+ Check::DuplicateDefaultValue => "duplicate_default_value",
+ Check::DuplicateImply => "duplicate_imply",
+ Check::DeadImply => "dead_imply",
+ Check::ReverseRange => "reverse_range",
+ }
+ }
+}
+
+#[derive(Debug)]
+pub(crate) struct ParseCheckError {
+ pub(crate) input: String,
+}
+
+impl std::fmt::Display for ParseCheckError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "unknown check '{}'", self.input)
+ }
+}
+
+impl std::error::Error for ParseCheckError {}
+
+impl FromStr for Check {
+ type Err = ParseCheckError;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ match name {
+ "dead_link" => Ok(Check::DeadLink),
+ "select_visible" => Ok(Check::SelectVisible),
+ "duplicate_dependency" => Ok(Check::DuplicateDependency),
+ "duplicate_range" => Ok(Check::DuplicateRange),
+ "dead_range" => Ok(Check::DeadRange),
+ "duplicate_select" => Ok(Check::DuplicateSelect),
+ "dead_select" => Ok(Check::DeadSelect),
+ "dead_default" => Ok(Check::DeadDefault),
+ "constant_condition" => Ok(Check::ConstantCondition),
+ "duplicate_default" => Ok(Check::DuplicateDefault),
+ "duplicate_default_value" => Ok(Check::DuplicateDefaultValue),
+ "duplicate_imply" => Ok(Check::DuplicateImply),
+ "dead_imply" => Ok(Check::DeadImply),
+ "reverse_range" => Ok(Check::ReverseRange),
+ _ => Err(ParseCheckError {
+ input: name.to_string(),
+ }),
+ }
+ }
+}
+
+/// The set of checks enabled for one analysis run.
+#[derive(Debug)]
+pub(crate) struct AnalysisArgs {
+ enabled_checks: HashSet<Check>,
+}
+
+impl AnalysisArgs {
+ /// Creates an analysis configuration with no checks enabled.
+ pub(crate) fn new() -> Self {
+ Self {
+ enabled_checks: HashSet::new(),
+ }
+ }
+
+ /// Enables `check`, returning whether it was newly enabled.
+ pub(crate) fn enable_check(&mut self, check: Check) -> bool {
+ self.enabled_checks.insert(check)
+ }
+
+ /// Disables `check`, returning whether it had been enabled.
+ pub(crate) fn disable_check(&mut self, check: Check) -> bool {
+ self.enabled_checks.remove(&check)
+ }
+
+ /// Returns whether `check` is enabled.
+ pub(crate) fn is_enabled(&self, check: Check) -> bool {
+ self.enabled_checks.contains(&check)
+ }
+}
+
+/// Checks int/hex ranges with lower bound > upper bound.
+pub(crate) fn check_reverse_ranges(
+ arch: &str,
+ var_symbol: &str,
+ info: &AttributeDef,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ for range in &info.ranges {
+ // Return an error when a bound cannot be parsed as an `i128`.
+ fn range_bound_to_int(range_bound: &RangeBound) -> Result<i128, ParseIntError> {
+ if range_bound.value.starts_with("0x") || range_bound.value.starts_with("0X") {
+ let trimmed = range_bound
+ .value
+ .trim_start_matches("0x")
+ .trim_start_matches("0X");
+ i128::from_str_radix(trimmed, 16)
+ } else {
+ range_bound.value.parse()
+ }
+ }
+
+ let maybe_lower_bound = range_bound_to_int(&range.lower_bound);
+ let maybe_upper_bound = range_bound_to_int(&range.upper_bound);
+
+ match (maybe_lower_bound, maybe_upper_bound) {
+ (Ok(lower_bound), Ok(upper_bound)) => {
+ if lower_bound > upper_bound {
+ let message = format!(
+ "reverse range {} for config option: {}, no value is valid",
+ range, var_symbol,
+ );
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::ReverseRange,
+ symbol: Some(var_symbol.to_owned()),
+ arch: arch.to_owned(),
+ message,
+ });
+ }
+ }
+ (Result::Err(_), _) | (_, Result::Err(_)) => continue,
+ }
+ }
+
+ findings
+}
+
+pub(crate) fn check_constant_conditions(
+ arch: &str,
+ var_symbol: &str,
+ info: &AttributeDef,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let default_conditions: Vec<&Expression> = info
+ .defaults
+ .iter()
+ .filter_map(|conditional_default| conditional_default.r#if.as_ref())
+ .collect();
+
+ check_conditions(
+ arch,
+ &mut findings,
+ var_symbol,
+ &info.dependencies,
+ default_conditions,
+ "default",
+ );
+
+ let select_conditions: Vec<&Expression> = info
+ .selects
+ .iter()
+ .filter_map(|conditional_select| conditional_select.1.as_ref())
+ .collect();
+
+ check_conditions(
+ arch,
+ &mut findings,
+ var_symbol,
+ &info.dependencies,
+ select_conditions,
+ "select",
+ );
+
+ let imply_conditions: Vec<&Expression> = info
+ .implies
+ .iter()
+ .filter_map(|imp| imp.1.as_ref())
+ .collect();
+
+ check_conditions(
+ arch,
+ &mut findings,
+ var_symbol,
+ &info.dependencies,
+ imply_conditions,
+ "imply",
+ );
+
+ let range_conditions: Vec<&Expression> = info
+ .ranges
+ .iter()
+ .filter_map(|conditional_range| conditional_range.r#if.as_ref())
+ .collect();
+
+ check_conditions(
+ arch,
+ &mut findings,
+ var_symbol,
+ &info.dependencies,
+ range_conditions,
+ "range",
+ );
+
+ fn check_conditions(
+ arch: &str,
+ findings: &mut Vec<Finding>,
+ symbol: &str,
+ dependencies: &[Expression],
+ attribute_conditions: Vec<&Expression>,
+ context: &str,
+ ) {
+ for attribute_condition in attribute_conditions {
+ if dependencies.contains(attribute_condition) {
+ let message = format!(
+ "constant {} condition 'if {}' for config option: {}, this condition is a dependency and will always be true",
+ context,
+ attribute_condition,
+ symbol,
+ );
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::ConstantCondition,
+ symbol: Some(symbol.to_owned()),
+ arch: arch.to_owned(),
+ message,
+ });
+ } else if dependencies
+ .iter()
+ .any(|dependency| attribute_condition.is_negation_of(dependency))
+ {
+ let message = format!(
+ "constant {} condition 'if {}' for config option: {}, this condition negates a dependency and will always be false",
+ context, attribute_condition, symbol,
+ );
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::ConstantCondition,
+ symbol: Some(symbol.to_owned()),
+ arch: arch.to_owned(),
+ message,
+ });
+ }
+ }
+ }
+ findings
+}
+
+pub(crate) fn check_variable_info(
+ args: &AnalysisArgs,
+ var_symbol: &str,
+ arch: &str,
+ info: &AttributeDef,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ if args.is_enabled(Check::DuplicateDependency) {
+ findings.extend(check_duplicate_dependencies(arch, var_symbol, info));
+ }
+
+ if args.is_enabled(Check::DuplicateImply) || args.is_enabled(Check::DeadImply) {
+ findings.extend(check_implies(arch, var_symbol, info, args));
+ }
+
+ if args.is_enabled(Check::DuplicateRange) || args.is_enabled(Check::DeadRange) {
+ findings.extend(check_ranges(arch, var_symbol, info, args));
+ }
+
+ if args.is_enabled(Check::DuplicateSelect) || args.is_enabled(Check::DeadSelect) {
+ findings.extend(check_selects(arch, var_symbol, info, args));
+ }
+
+ if args.is_enabled(Check::ConstantCondition) {
+ findings.extend(check_constant_conditions(arch, var_symbol, info));
+ }
+
+ if args.is_enabled(Check::DeadDefault)
+ || args.is_enabled(Check::DuplicateDefault)
+ || args.is_enabled(Check::DuplicateDefaultValue)
+ {
+ findings.extend(check_defaults(arch, var_symbol, info, args));
+ }
+
+ if args.is_enabled(Check::ReverseRange) {
+ findings.extend(check_reverse_ranges(arch, var_symbol, info));
+ }
+
+ findings
+}
+
+pub(crate) fn check_select_visible(
+ var_symbol: &str,
+ info: &SymbolInfo,
+ arch: &str,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ if info.selected_by.is_empty() {
+ return Vec::new();
+ }
+
+ for (selector, select_conditions) in &info.selected_by {
+ for _condition in select_conditions {
+ let message = format!(
+ "selects the visible {}; consider using 'depends on' or 'imply' instead",
+ var_symbol
+ );
+
+ for (if_conditions, attributes) in &info.attribute_defs {
+ if if_conditions.is_empty() && attributes.visibility.is_empty() {
+ // Empty visibility makes the symbol unconditionally
+ // visible (for this architecture).
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::SelectVisible,
+ symbol: Some(selector.to_owned()),
+ message: message.clone(),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+
+ findings
+}
+
+fn is_duplicate<T: Eq + std::hash::Hash>(set: &mut HashSet<T>, key: T) -> bool {
+ !set.insert(key)
+}
+
+fn check_duplicate_dependencies(arch: &str, var_symbol: &str, info: &AttributeDef) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let mut seen = HashSet::new();
+
+ for dep in &info.dependencies {
+ if is_duplicate(&mut seen, dep.to_string()) {
+ let message = format!("duplicate dependency on {dep}");
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateDependency,
+ symbol: Some(var_symbol.to_owned()),
+ message,
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ findings
+}
+
+/// Checks for `duplicate_imply` and `dead_imply`.
+fn check_implies(
+ arch: &str,
+ var_symbol: &str,
+ info: &AttributeDef,
+ args: &AnalysisArgs,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ // Symbols implied unconditionally.
+ let mut unconditional: HashSet<String> = HashSet::new();
+
+ // Each tuple represents (symbol, implication condition).
+ let mut conditional: HashSet<(String, String)> = HashSet::new();
+
+ for (imply_var, imply_cond) in &info.implies {
+ match &imply_cond {
+ Some(cond) => {
+ let cond_str = cond.to_string();
+
+ // Report duplicate conditional implies. The insertion must
+ // happen even when the check is disabled.
+ if !conditional.insert((imply_var.clone(), cond_str.clone()))
+ && args.is_enabled(Check::DuplicateImply)
+ {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateImply,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!(
+ "duplicate imply of {} with condition {}",
+ imply_var, cond_str
+ ),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // An unconditional imply makes this conditional one dead.
+ if unconditional.contains(imply_var) && args.is_enabled(Check::DeadImply) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadImply,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead imply of {}", imply_var),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ None => {
+ // Report duplicate unconditional implies. The insertion must
+ // happen even when the check is disabled.
+ if !unconditional.insert(imply_var.clone())
+ && args.is_enabled(Check::DuplicateImply)
+ {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateImply,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate imply of {}", imply_var),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // An unconditional imply makes this conditional one dead.
+ if args.is_enabled(Check::DeadImply) {
+ for (sym, _) in &conditional {
+ if sym == imply_var {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadImply,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead imply of {}", imply_var),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ findings
+}
+
+/// Checks `duplicate_range` and `dead_range`.
+fn check_ranges(
+ arch: &str,
+ var_symbol: &str,
+ info: &AttributeDef,
+ args: &AnalysisArgs,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ // Unconditional ranges, for duplicate detection.
+ let mut unconditional: HashSet<String> = HashSet::new();
+
+ // Each tuple represents (range bounds, condition).
+ let mut conditional: HashSet<(String, String)> = HashSet::new();
+
+ // Kconfig uses the first range whose condition is satisfied. Once an
+ // unconditional range has been reached, every non-duplicate range after it
+ // is unreachable, regardless of its bounds.
+ let mut seen_unconditional = false;
+
+ for range in &info.ranges {
+ // Use both bounds to identify a range.
+ let range_key = format!("{} {}", range.lower_bound, range.upper_bound);
+
+ let duplicate = match &range.r#if {
+ Some(cond) => {
+ let cond_str = cond.to_string();
+
+ // Report duplicate conditional ranges. The insertion must
+ // happen even when the check is disabled.
+ let duplicate = !conditional.insert((range_key, cond_str.clone()));
+ if duplicate && args.is_enabled(Check::DuplicateRange) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateRange,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate range {} with condition {}", range, cond_str),
+ arch: arch.to_owned(),
+ });
+ }
+ duplicate
+ }
+
+ None => {
+ // Report duplicate unconditional ranges. The insertion must
+ // happen even when the check is disabled.
+ let duplicate = !unconditional.insert(range_key);
+ if duplicate && args.is_enabled(Check::DuplicateRange) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateRange,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate range {}", range),
+ arch: arch.to_owned(),
+ });
+ }
+ duplicate
+ }
+ };
+
+ if seen_unconditional && !duplicate && args.is_enabled(Check::DeadRange) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadRange,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead range of {}", range),
+ arch: arch.to_owned(),
+ });
+ }
+
+ if range.r#if.is_none() {
+ seen_unconditional = true;
+ }
+ }
+
+ findings
+}
+
+/// Checks for `duplicate_select` and `dead_select`.
+fn check_selects(
+ arch: &str,
+ var_symbol: &str,
+ info: &AttributeDef,
+ args: &AnalysisArgs,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ // Unconditional selects.
+ let mut unconditional: HashSet<String> = HashSet::new();
+
+ // Each tuple represents (symbol, condition).
+ let mut conditional: HashSet<(String, String)> = HashSet::new();
+
+ for select in &info.selects {
+ let select_var = select.0.clone();
+
+ match &select.1 {
+ Some(cond) => {
+ let cond_str = cond.to_string();
+
+ // Report duplicate conditional selects. The insertion must
+ // happen even when the check is disabled.
+ if !conditional.insert((select_var.clone(), cond_str.clone()))
+ && args.is_enabled(Check::DuplicateSelect)
+ {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateSelect,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!(
+ "duplicate select of {} with condition {}",
+ select.0, cond_str
+ ),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // An unconditional select makes this conditional one dead.
+ if unconditional.contains(&select_var) && args.is_enabled(Check::DeadSelect) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadSelect,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead select of {}", select.0),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ None => {
+ // Report duplicate unconditional selects. The insertion must
+ // happen even when the check is disabled.
+ if !unconditional.insert(select_var.clone())
+ && args.is_enabled(Check::DuplicateSelect)
+ {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateSelect,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate select of {}", select.0),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // An unconditional select makes this conditional one dead.
+ if args.is_enabled(Check::DeadSelect) {
+ for (sym, _) in &conditional {
+ if sym == &select_var {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadSelect,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead select of {}", select.0),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ findings
+}
+
+fn check_defaults(
+ arch: &str,
+ var_symbol: &str,
+ info: &AttributeDef,
+ args: &AnalysisArgs,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let mut seen_conditions = HashSet::new();
+ let mut seen_values = HashSet::new();
+ let mut seen_unconditional_values: HashSet<String> = HashSet::new();
+ let mut already_unconditional = false;
+
+ for default in &info.defaults {
+ let val_str = default.expression.to_string();
+
+ let has_real_condition = match &default.r#if {
+ Some(cond) => {
+ let cond_str = cond.to_string();
+ !cond_str.is_empty()
+ }
+ None => false,
+ };
+
+ let is_value_dup = if has_real_condition {
+ is_duplicate(&mut seen_values, val_str.clone())
+ } else {
+ false
+ };
+
+ // Any default following an unconditional one is dead or a duplicate.
+ if already_unconditional {
+ let duplicate_unconditional =
+ default.r#if.is_none() && seen_unconditional_values.contains(&val_str);
+
+ if duplicate_unconditional && args.is_enabled(Check::DuplicateDefault) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateDefault,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate default of {}", val_str),
+ arch: arch.to_owned(),
+ });
+ } else if !duplicate_unconditional && args.is_enabled(Check::DeadDefault) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadDefault,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead default of {}", val_str),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ if args.is_enabled(Check::DuplicateDefaultValue) {
+ if default.r#if.is_some() && is_value_dup {
+ findings.push(Finding {
+ severity: Severity::Style,
+ check: Check::DuplicateDefaultValue,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!(
+ "duplicate default value of {}; consider combining the conditions with a logical-or: ||",
+ val_str
+ ),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ match &default.r#if {
+ Some(cond) => {
+ if is_duplicate(&mut seen_conditions, cond.to_string()) {
+ if is_value_dup {
+ if args.is_enabled(Check::DuplicateDefault) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateDefault,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate default of {}", val_str),
+ arch: arch.to_owned(),
+ });
+ }
+ } else {
+ if args.is_enabled(Check::DeadDefault) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadDefault,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead default of {}", val_str),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+ None => {
+ seen_unconditional_values.insert(val_str);
+ already_unconditional = true;
+ }
+ }
+ }
+
+ findings
+}
diff --git a/scripts/kconfig/kconfirm/dead_links.rs b/scripts/kconfig/kconfirm/dead_links.rs
new file mode 100644
index 000000000000..205ba820fa44
--- /dev/null
+++ b/scripts/kconfig/kconfirm/dead_links.rs
@@ -0,0 +1,230 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Julian Braha <julianbraha@gmail.com>
+ */
+use std::{
+ collections::HashSet,
+ process::{
+ Command,
+ Stdio, //
+ },
+};
+
+struct HttpResponse {
+ response_code: u16,
+ location: Option<String>,
+}
+
+/// Verifies that the `curl` command is available.
+pub(crate) fn check_curl_available() -> Result<(), String> {
+ match Command::new("curl")
+ .arg("--version")
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status()
+ {
+ Ok(status) if status.success() => Ok(()),
+ Ok(status) => Err(format!("`curl --version` exited with {status}")),
+ Err(error) => Err(format!("failed to execute `curl`: {error}")),
+ }
+}
+
+/// Performs an HTTP `HEAD` request without following redirects.
+fn head_request(url: &str) -> Result<HttpResponse, String> {
+ let output = Command::new("curl")
+ .args([
+ "--head",
+ "--silent",
+ "--show-error",
+ "--max-time",
+ "10",
+ "--user-agent",
+ "link-checker/1.0",
+ "--output",
+ "/dev/null",
+ "--write-out",
+ "%{http_code}\n%{redirect_url}",
+ "--url",
+ url,
+ ])
+ .output()
+ .map_err(|error| format!("failed to execute `curl`: {error}"))?;
+
+ if !output.status.success() {
+ let error = String::from_utf8_lossy(&output.stderr);
+ let error = error.trim();
+
+ return if error.is_empty() {
+ Err(format!("`curl` exited with {}", output.status))
+ } else {
+ Err(error.to_owned())
+ };
+ }
+
+ let output = String::from_utf8(output.stdout)
+ .map_err(|error| format!("`curl` produced invalid UTF-8: {error}"))?;
+ let (response_code, location) = output
+ .split_once('\n')
+ .ok_or("`curl` did not produce an HTTP response code")?;
+ let response_code = response_code
+ .parse()
+ .map_err(|error| format!("invalid HTTP response code from `curl`: {error}"))?;
+ let location = location.trim();
+
+ Ok(HttpResponse {
+ response_code,
+ location: (!location.is_empty()).then(|| location.to_owned()),
+ })
+}
+
+/// Result of checking whether a help-text link is reachable.
+#[derive(PartialEq, Debug)]
+pub(crate) enum LinkStatus {
+ Ok,
+ ProbablyBlocked,
+ Redirected(String),
+ NotFound,
+ ServerError,
+ Unreachable(String),
+}
+
+impl LinkStatus {
+ /// Returns a stable diagnostic label for the status.
+ pub(crate) fn as_str(&self) -> &'static str {
+ match self {
+ Self::Ok => "ok",
+ Self::ProbablyBlocked => "probably blocked",
+ Self::Redirected(_) => "redirected",
+ Self::NotFound => "not found",
+ Self::ServerError => "server error",
+ Self::Unreachable(_) => "unreachable",
+ }
+ }
+}
+
+/// Checks one link without following redirects.
+pub(crate) fn check_link(url: &str) -> LinkStatus {
+ let Some((scheme, _)) = url.split_once("://") else {
+ return LinkStatus::Unreachable("invalid URL".into());
+ };
+
+ if scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https") {
+ check_http(url)
+ } else {
+ LinkStatus::Unreachable("unsupported URL scheme".into())
+ }
+}
+
+fn check_http(url: &str) -> LinkStatus {
+ let response = match head_request(url) {
+ Ok(r) => r,
+ Err(e) => return LinkStatus::Unreachable(e),
+ };
+
+ match response.response_code {
+ 200..=299 => LinkStatus::Ok,
+
+ 301 | 302 | 303 | 307 | 308 => {
+ LinkStatus::Redirected(response.location.unwrap_or_else(|| "unknown".into()))
+ }
+
+ 403 | 429 => LinkStatus::ProbablyBlocked,
+
+ 404 => LinkStatus::NotFound,
+
+ 500..=599 => LinkStatus::ServerError,
+
+ _ => LinkStatus::ProbablyBlocked,
+ }
+}
+
+/// Extracts links from a Kconfig help text.
+pub(crate) fn find_links(text: &str) -> Vec<String> {
+ fn is_scheme_char(c: u8) -> bool {
+ c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.')
+ }
+
+ fn is_url_terminator(c: u8) -> bool {
+ c.is_ascii_whitespace() || matches!(c, b'"' | b'\'' | b'<' | b'>')
+ }
+
+ let bytes = text.as_bytes();
+
+ let mut links = Vec::new();
+ let mut seen = HashSet::new();
+
+ let mut i = 0;
+
+ while i + 3 < bytes.len() {
+ if bytes[i] == b':' && bytes[i + 1] == b'/' && bytes[i + 2] == b'/' {
+ // Walk backward to find the start of the scheme.
+ let mut start = i;
+
+ while start > 0 && is_scheme_char(bytes[start - 1]) {
+ start -= 1;
+ }
+
+ // Require a non-empty scheme.
+ if start == i {
+ i += 3;
+ continue;
+ }
+
+ // Require an alphabetic first character.
+ if !bytes[start].is_ascii_alphabetic() {
+ i += 3;
+ continue;
+ }
+
+ // Walk forward to the end of the URL.
+ let mut end = i + 3;
+
+ while end < bytes.len() && !is_url_terminator(bytes[end]) {
+ end += 1;
+ }
+
+ let mut url = &text[start..end];
+
+ // Trim trailing punctuation.
+ url = url.trim_end_matches(&['.', ',', ';', ':', '!', '?'][..]);
+
+ // Trim unmatched Markdown delimiters.
+ while let Some(last) = url.chars().last() {
+ let trim = match last {
+ ')' => url.matches('(').count() < url.matches(')').count(),
+
+ ']' => url.matches('[').count() < url.matches(']').count(),
+
+ '}' => url.matches('{').count() < url.matches('}').count(),
+
+ _ => false,
+ };
+
+ if trim {
+ url = &url[..url.len() - last.len_utf8()];
+ } else {
+ break;
+ }
+ }
+
+ let Some((scheme, _)) = url.split_once("://") else {
+ i = end;
+ continue;
+ };
+ if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {
+ i = end;
+ continue;
+ }
+
+ if seen.insert(url) {
+ links.push(url.to_string());
+ }
+
+ i = end;
+ } else {
+ i += 1;
+ }
+ }
+
+ links
+}
diff --git a/scripts/kconfig/kconfirm/kconfirm-cfg.sh b/scripts/kconfig/kconfirm/kconfirm-cfg.sh
new file mode 100755
index 000000000000..0c5b0c91016a
--- /dev/null
+++ b/scripts/kconfig/kconfirm/kconfirm-cfg.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+#
+# kconfirm is written in Rust, so check that the compiler is available and
+# recent enough. Bindgen is also required because kconfirm uses generated
+# Rust bindings to access the Kconfig parser.
+
+set -eu
+
+min_tool_version=$(dirname "$0")/../../min-tool-version.sh
+
+rustc=${RUSTC:-rustc}
+bindgen=${BINDGEN:-bindgen}
+
+fail()
+{
+ echo >&2 "*"
+ echo >&2 "* $1"
+ echo >&2 "*"
+ echo >&2 "* kconfirm is written in Rust. For instructions on installing"
+ echo >&2 "* the Rust toolchain, see Documentation/rust/quick-start.rst;"
+ echo >&2 "* for kconfirm's requirements, see"
+ echo >&2 "* Documentation/dev-tools/kconfirm.rst."
+ echo >&2 "*"
+ exit 1
+}
+
+if ! command -v "$rustc" >/dev/null; then
+ fail "Unable to find the Rust compiler '$rustc'."
+fi
+
+if ! command -v "$bindgen" >/dev/null; then
+ fail "Unable to find the Rust bindings generator '$bindgen'."
+fi
+
+rustc_version=$(LC_ALL=C "$rustc" --version 2>/dev/null \
+ | sed -nE '1s:.*rustc ([0-9]+\.[0-9]+\.[0-9]+).*:\1:p')
+if [ -z "$rustc_version" ]; then
+ fail "Running '$rustc --version' did not produce an output."
+fi
+
+rustc_min_version=$("$min_tool_version" rustc)
+if ! printf '%s\n%s\n' "$rustc_min_version" "$rustc_version" | sort -CV; then
+ fail "'$rustc' is too old: $rustc_version < $rustc_min_version required."
+fi
+
+bindgen_version=$(LC_ALL=C "$bindgen" --version 2>/dev/null \
+ | sed -nE '1s:.*bindgen ([0-9]+\.[0-9]+\.[0-9]+).*:\1:p')
+if [ -z "$bindgen_version" ]; then
+ fail "Running '$bindgen --version' did not produce an output."
+fi
+
+bindgen_min_version=$("$min_tool_version" bindgen)
+if ! printf '%s\n%s\n' "$bindgen_min_version" "$bindgen_version" | sort -CV; then
+ fail "'$bindgen' is too old: $bindgen_version < $bindgen_min_version required."
+fi
diff --git a/scripts/kconfig/kconfirm/kconfirm.rs b/scripts/kconfig/kconfirm/kconfirm.rs
new file mode 100644
index 000000000000..ceb4dd83c5bb
--- /dev/null
+++ b/scripts/kconfig/kconfirm/kconfirm.rs
@@ -0,0 +1,278 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Julian Braha <julianbraha@gmail.com>
+ */
+//! A static analyzer for Kconfig.
+
+use crate::{
+ analyze::analyze,
+ arch::{
+ arch_dir_to_config,
+ arch_to_srcarch,
+ available_architectures, //
+ },
+ checks::{
+ check_select_visible,
+ check_variable_info,
+ AnalysisArgs,
+ Check, //
+ },
+ kconfig::parse_kconfig,
+ output::{
+ print_findings,
+ Finding, //
+ },
+ symbol_table::SymbolTable, //
+};
+use std::{
+ env,
+ ffi::{
+ CStr,
+ CString,
+ OsString, //
+ },
+ fs,
+ io,
+ path::PathBuf,
+ str::FromStr, //
+};
+
+mod analyze;
+mod arch;
+mod checks;
+mod dead_links;
+#[path = "../kconfig.rs"]
+mod kconfig;
+mod output;
+mod symbol_table;
+
+const USAGE: &str = "\
+Usage: kconfirm --linux-path PATH [OPTION]...
+
+Statically analyze the Kconfig files of a kernel source tree.
+
+Options:
+ -l, --linux-path PATH kernel source tree to analyze
+ -e, --enable-check CHECK,... checks to run on top of the default set
+ -d, --disable-check CHECK,... checks to drop from the default set
+ -k, --kconfig FILE entry Kconfig file (default is the root Kconfig)
+ -h, --help display this help and exit
+
+See Documentation/dev-tools/kconfirm.rst for the list of checks.
+";
+
+/// Runs the enabled checks on the Kconfig parse tree.
+///
+/// # Panics
+///
+/// Panics if traversing the tree encounters an unsupported node or the
+/// parser fails.
+fn check_kconfig(args: &AnalysisArgs, kconfig: &CStr, arch: &str) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let mut symbol_table = SymbolTable::new();
+ let entries = parse_kconfig(kconfig);
+ findings.extend(analyze(args, &mut symbol_table, arch, entries));
+
+ for (var_symbol, type_info) in &symbol_table.inner {
+ for (_definition_condition, info) in &type_info.attribute_defs {
+ findings.extend(check_variable_info(args, var_symbol, arch, info));
+ }
+
+ if args.is_enabled(Check::SelectVisible) {
+ findings.extend(check_select_visible(var_symbol, type_info, arch));
+ }
+ }
+
+ findings
+}
+
+fn split_csv_arg(dst: &mut Vec<String>, value: &str) {
+ dst.extend(
+ value
+ .split(',')
+ .filter(|s| !s.is_empty())
+ .map(|s| s.to_string()),
+ );
+}
+
+#[derive(Debug)]
+struct Args {
+ linux_path: PathBuf,
+ kconfig: CString,
+ enable_check: Vec<String>,
+ disable_check: Vec<String>,
+}
+
+fn utf8_argument(argument: OsString) -> Result<String, String> {
+ argument
+ .into_string()
+ .map_err(|argument| format!("argument is not valid UTF-8: {:?}", argument))
+}
+
+/// Returns the value of an option, either the one attached to it with `=` or
+/// packed behind a short name, else the next argument.
+fn option_value(
+ option: &str,
+ attached: Option<String>,
+ arguments: &mut impl Iterator<Item = OsString>,
+) -> Result<String, String> {
+ match attached {
+ Some(value) => Ok(value),
+ None => match arguments.next() {
+ Some(value) => utf8_argument(value),
+ None => Err(format!("{option} requires an argument")),
+ },
+ }
+}
+
+fn parse_args() -> Result<Args, String> {
+ let mut linux_path: Option<PathBuf> = None;
+ let mut kconfig = String::from("Kconfig");
+ let mut enable_check = Vec::new();
+ let mut disable_check = Vec::new();
+
+ let mut arguments = env::args_os().skip(1);
+ while let Some(argument) = arguments.next() {
+ let argument = utf8_argument(argument)?;
+
+ // Accept both `--option value` and `--option=value` for long options,
+ // and both `-o value` and `-ovalue` for short ones.
+ let (option, attached) = if let Some(name) = argument.strip_prefix("--") {
+ match name.split_once('=') {
+ Some((name, value)) => (format!("--{name}"), Some(value.to_string())),
+ None => (argument.clone(), None),
+ }
+ } else if argument.starts_with('-') && argument.len() > 2 {
+ (argument[..2].to_string(), Some(argument[2..].to_string()))
+ } else {
+ (argument.clone(), None)
+ };
+
+ match option.as_str() {
+ "-h" | "--help" => {
+ print!("{USAGE}");
+ std::process::exit(0);
+ }
+ "-l" | "--linux-path" => {
+ let value = option_value(&option, attached, &mut arguments)?;
+ linux_path = Some(PathBuf::from(value));
+ }
+ "-k" | "--kconfig" => {
+ kconfig = option_value(&option, attached, &mut arguments)?;
+ }
+ "-e" | "--enable-check" => {
+ let value = option_value(&option, attached, &mut arguments)?;
+ split_csv_arg(&mut enable_check, &value);
+ }
+ "-d" | "--disable-check" => {
+ let value = option_value(&option, attached, &mut arguments)?;
+ split_csv_arg(&mut disable_check, &value);
+ }
+ _ => return Err(format!("unrecognized option '{option}'")),
+ }
+ }
+
+ let linux_path = linux_path.ok_or("--linux-path is required")?;
+
+ let kconfig = CString::new(kconfig).map_err(|_| "--kconfig contains a NUL byte".to_string())?;
+
+ Ok(Args {
+ linux_path,
+ kconfig,
+ enable_check,
+ disable_check,
+ })
+}
+
+fn source_architecture() -> Result<String, String> {
+ let arch = env::var_os("ARCH").ok_or("ARCH environment variable is required")?;
+ let arch = arch
+ .into_string()
+ .map_err(|arch| format!("ARCH is not valid UTF-8: {arch:?}"))?;
+ if arch.is_empty() {
+ return Err("ARCH environment variable is empty".into());
+ }
+
+ Ok(arch_to_srcarch(&arch).to_owned())
+}
+
+fn main() -> io::Result<()> {
+ let cli_args = parse_args().unwrap_or_else(|e| {
+ eprintln!("error: {e}");
+ eprint!("{USAGE}");
+ std::process::exit(1);
+ });
+ let mut analysis_args = AnalysisArgs::new();
+ for check in [
+ Check::DuplicateDependency,
+ Check::DuplicateRange,
+ Check::DeadRange,
+ Check::DuplicateSelect,
+ Check::DeadSelect,
+ Check::DeadDefault,
+ Check::ConstantCondition,
+ Check::DuplicateDefault,
+ Check::DuplicateImply,
+ Check::DeadImply,
+ Check::ReverseRange,
+ ] {
+ analysis_args.enable_check(check);
+ }
+
+ // `--enable-check`.
+ for name in &cli_args.enable_check {
+ if let Ok(c) = Check::from_str(name) {
+ analysis_args.enable_check(c);
+ } else {
+ eprintln!("Error: check {} does not exist", name);
+ std::process::exit(1);
+ }
+ }
+
+ // `--disable-check`.
+ for name in &cli_args.disable_check {
+ if let Ok(c) = Check::from_str(name) {
+ analysis_args.disable_check(c);
+ } else {
+ eprintln!("Error: check {} does not exist", name);
+ std::process::exit(1);
+ }
+ }
+
+ if analysis_args.is_enabled(Check::DeadLink) {
+ dead_links::check_curl_available().unwrap_or_else(|error| {
+ eprintln!("error: dead_link requires the `curl` command: {error}");
+ std::process::exit(1);
+ });
+ }
+
+ let linux_path = fs::canonicalize(cli_args.linux_path)?;
+ let arch = source_architecture().unwrap_or_else(|error| {
+ eprintln!("error: {error}");
+ std::process::exit(1);
+ });
+ let available_arches = available_architectures(&linux_path)?;
+ if !available_arches.contains(&arch) {
+ eprintln!("Error: unexpected architecture from ARCH, expected one of:");
+ for available_arch in &available_arches {
+ eprint!("{available_arch} ");
+ }
+ eprintln!();
+ std::process::exit(1);
+ }
+
+ env::set_current_dir(&linux_path)?;
+ // SAFETY: kconfirm is single-threaded, so changing its environment cannot
+ // race another environment reader or writer.
+ unsafe {
+ env::set_var("srctree", &linux_path);
+ env::set_var("SRCARCH", &arch);
+ if arch == "um" && env::var_os("HEADER_ARCH").is_none() {
+ env::set_var("HEADER_ARCH", "x86");
+ }
+ }
+ let arch_config = arch_dir_to_config(&arch);
+ let findings = check_kconfig(&analysis_args, &cli_args.kconfig, &arch_config);
+ print_findings(findings);
+ Ok(())
+}
diff --git a/scripts/kconfig/kconfirm/output.rs b/scripts/kconfig/kconfirm/output.rs
new file mode 100644
index 000000000000..dae3c903faca
--- /dev/null
+++ b/scripts/kconfig/kconfirm/output.rs
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Julian Braha <julianbraha@gmail.com>
+ */
+use crate::Check;
+use std::fmt;
+
+/// Diagnostic severity, ordered from most to least severe.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub(crate) enum Severity {
+ Warning,
+ Style,
+}
+
+/// One diagnostic emitted by a Kconfig check.
+#[derive(Debug)]
+pub(crate) struct Finding {
+ pub(crate) severity: Severity,
+ pub(crate) check: Check,
+ pub(crate) symbol: Option<String>,
+ pub(crate) message: String,
+ pub(crate) arch: String,
+}
+
+impl fmt::Display for Finding {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match &self.symbol {
+ Some(s) => write!(
+ f,
+ "{} [{}] [{}] config {}: {}",
+ self.severity,
+ self.check.as_str(),
+ self.arch,
+ s,
+ self.message
+ ),
+ None => write!(
+ f,
+ "{} [{}] [{}] {}",
+ self.severity,
+ self.check.as_str(),
+ self.arch,
+ self.message
+ ),
+ }
+ }
+}
+
+/// Sorts, deduplicates, and prints findings.
+pub(crate) fn print_findings(mut findings: Vec<Finding>) {
+ findings.sort_by(|a, b| {
+ (
+ &a.severity,
+ a.check.as_str(),
+ &a.symbol,
+ &a.message,
+ &a.arch,
+ )
+ .cmp(&(
+ &b.severity,
+ b.check.as_str(),
+ &b.symbol,
+ &b.message,
+ &b.arch,
+ ))
+ });
+ findings.dedup_by(|a, b| {
+ a.severity == b.severity
+ && a.check.as_str() == b.check.as_str()
+ && a.symbol == b.symbol
+ && a.message == b.message
+ && a.arch == b.arch
+ });
+
+ for finding in findings {
+ println!("{finding}");
+ }
+}
+
+impl fmt::Display for Severity {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Severity::Warning => write!(f, "WARNING"),
+ Severity::Style => write!(f, "STYLE "),
+ }
+ }
+}
diff --git a/scripts/kconfig/kconfirm/symbol_table.rs b/scripts/kconfig/kconfirm/symbol_table.rs
new file mode 100644
index 000000000000..0b2cc1e296d5
--- /dev/null
+++ b/scripts/kconfig/kconfirm/symbol_table.rs
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Julian Braha <julianbraha@gmail.com>
+ */
+use crate::kconfig::{
+ DefaultAttribute,
+ Expression,
+ Range, //
+};
+use std::collections::{
+ hash_map,
+ HashMap, //
+};
+
+type KconfigSymbol = String;
+type Cond = Option<Expression>;
+
+/// Collected analysis information for one Kconfig symbol.
+#[derive(Debug, Clone)]
+pub(crate) struct SymbolInfo {
+ /// Selectors of this option (and their select-conditions).
+ pub(crate) selected_by: HashMap<KconfigSymbol, Vec<Cond>>,
+ /// Each partial or complete definition and their nesting conditions.
+ pub(crate) attribute_defs: Vec<(Vec<Expression>, AttributeDef)>,
+}
+
+/// The attributes of one partial or complete symbol definition.
+#[derive(Debug, Clone, Default)]
+pub(crate) struct AttributeDef {
+ pub(crate) dependencies: Vec<Expression>,
+ pub(crate) ranges: Vec<Range>,
+ pub(crate) defaults: Vec<DefaultAttribute>,
+ pub(crate) visibility: Vec<Option<Expression>>,
+ pub(crate) selects: Vec<(KconfigSymbol, Cond)>,
+ pub(crate) implies: Vec<(KconfigSymbol, Cond)>,
+}
+
+/// One symbol-table update produced while traversing a Kconfig entry.
+pub(crate) struct SymbolUpdate {
+ pub(crate) symbol: KconfigSymbol,
+ pub(crate) is_definition: bool,
+ pub(crate) attributes: AttributeDef,
+ pub(crate) definition_condition: Vec<Expression>,
+ pub(crate) selected_by: Option<(KconfigSymbol, Cond)>,
+}
+
+impl SymbolInfo {
+ fn new_empty() -> Self {
+ Self {
+ selected_by: HashMap::new(),
+ attribute_defs: Vec::new(),
+ }
+ }
+
+ fn insert(&mut self, update: SymbolUpdate) {
+ let SymbolUpdate {
+ symbol: _,
+ is_definition,
+ attributes,
+ definition_condition,
+ selected_by,
+ } = update;
+
+ if let Some((selector, condition)) = selected_by {
+ self.selected_by
+ .entry(selector)
+ .or_default()
+ .push(condition);
+ }
+
+ if is_definition {
+ self.attribute_defs.push((definition_condition, attributes));
+ }
+ }
+}
+
+/// The symbol table stores the definitions of each encountered config option.
+pub(crate) struct SymbolTable {
+ pub(crate) inner: HashMap<KconfigSymbol, SymbolInfo>,
+}
+
+impl SymbolTable {
+ pub(crate) fn new() -> Self {
+ SymbolTable {
+ inner: HashMap::new(),
+ }
+ }
+
+ pub(crate) fn merge_insert(&mut self, update: SymbolUpdate) {
+ let entry = self.inner.entry(update.symbol.clone());
+
+ match entry {
+ hash_map::Entry::Vacant(v) => {
+ let mut t = SymbolInfo::new_empty();
+ t.insert(update);
+ v.insert(t);
+ }
+
+ hash_map::Entry::Occupied(mut o) => {
+ let t = o.get_mut();
+ t.insert(update);
+ }
+ }
+ }
+}
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v4 3/5] kconfirm: add tests
2026-07-27 0:16 [PATCH v4 0/5] add kconfirm Julian Braha
2026-07-27 0:16 ` [PATCH v4 1/5] kconfig: add add another callback to the parser to view raw parse tree Julian Braha
2026-07-27 0:16 ` [PATCH 2/5] kconfig: add kconfirm Julian Braha
@ 2026-07-27 0:16 ` Julian Braha
2026-07-27 0:16 ` [PATCH v4 4/5] Documentation: add kconfirm Julian Braha
` (2 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Julian Braha @ 2026-07-27 0:16 UTC (permalink / raw)
To: nathan, nsc
Cc: ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs, andrew.jones,
masahiroy, corbet, qingfang.deng, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
Add tests for kconfirm using pytest, as Kconfig does. The tests cover all
of kconfirm’s checks, and any additional, future checks need to add tests
as well.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Julian Braha <julianbraha@gmail.com>
---
.../kconfig/kconfirm/tests/arch/arm/Kconfig | 9 +
.../kconfirm/tests/arch/powerpc/Kconfig | 4 +
.../kconfig/kconfirm/tests/arch/riscv/Kconfig | 9 +
.../kconfig/kconfirm/tests/arch/sh/Kconfig | 4 +
.../kconfirm/tests/arch/testarch/Kconfig | 4 +
.../kconfig/kconfirm/tests/arch/um/Kconfig | 4 +
.../kconfig/kconfirm/tests/arch/x86/Kconfig | 4 +
.../kconfirm/tests/architecture.Kconfig | 4 +
.../tests/architecture_common.Kconfig | 19 +
.../kconfirm/tests/conditional_prompt.Kconfig | 17 +
scripts/kconfig/kconfirm/tests/conftest.py | 93 +++++
...nt_condition_negative_expression_1.Kconfig | 13 +
...nt_condition_negative_expression_2.Kconfig | 13 +
...nstant_condition_negative_symbol_1.Kconfig | 13 +
...nstant_condition_negative_symbol_2.Kconfig | 13 +
.../kconfig/kconfirm/tests/dead_link.Kconfig | 12 +
.../tests/default_categorization.Kconfig | 20 +
scripts/kconfig/kconfirm/tests/pytest.ini | 2 +
scripts/kconfig/kconfirm/tests/ranges.Kconfig | 39 ++
.../kconfirm/tests/select_imply.Kconfig | 28 ++
.../kconfig/kconfirm/tests/test_kconfirm.py | 358 ++++++++++++++++++
21 files changed, 682 insertions(+)
create mode 100644 scripts/kconfig/kconfirm/tests/arch/arm/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/powerpc/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/riscv/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/sh/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/testarch/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/um/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/arch/x86/Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/architecture.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/architecture_common.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/conditional_prompt.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/conftest.py
create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_1.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_2.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_1.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_2.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/dead_link.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/default_categorization.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/pytest.ini
create mode 100644 scripts/kconfig/kconfirm/tests/ranges.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/select_imply.Kconfig
create mode 100644 scripts/kconfig/kconfirm/tests/test_kconfirm.py
diff --git a/scripts/kconfig/kconfirm/tests/arch/arm/Kconfig b/scripts/kconfig/kconfirm/tests/arch/arm/Kconfig
new file mode 100644
index 000000000000..5922782a5f4d
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/arch/arm/Kconfig
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+source "architecture_common.Kconfig"
+
+config ARM_ONLY
+ bool
+ depends on ARM_PREREQUISITE
+ depends on ARM_PREREQUISITE
diff --git a/scripts/kconfig/kconfirm/tests/arch/powerpc/Kconfig b/scripts/kconfig/kconfirm/tests/arch/powerpc/Kconfig
new file mode 100644
index 000000000000..45e94dc6cec6
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/arch/powerpc/Kconfig
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+source "architecture_common.Kconfig"
diff --git a/scripts/kconfig/kconfirm/tests/arch/riscv/Kconfig b/scripts/kconfig/kconfirm/tests/arch/riscv/Kconfig
new file mode 100644
index 000000000000..9f760f673064
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/arch/riscv/Kconfig
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+source "architecture_common.Kconfig"
+
+config RISCV_ONLY
+ bool
+ depends on RISCV_PREREQUISITE
+ depends on RISCV_PREREQUISITE
diff --git a/scripts/kconfig/kconfirm/tests/arch/sh/Kconfig b/scripts/kconfig/kconfirm/tests/arch/sh/Kconfig
new file mode 100644
index 000000000000..45e94dc6cec6
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/arch/sh/Kconfig
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+source "architecture_common.Kconfig"
diff --git a/scripts/kconfig/kconfirm/tests/arch/testarch/Kconfig b/scripts/kconfig/kconfirm/tests/arch/testarch/Kconfig
new file mode 100644
index 000000000000..bb5c0e4b3829
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/arch/testarch/Kconfig
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+# Dynamically discovered test architecture.
diff --git a/scripts/kconfig/kconfirm/tests/arch/um/Kconfig b/scripts/kconfig/kconfirm/tests/arch/um/Kconfig
new file mode 100644
index 000000000000..45e94dc6cec6
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/arch/um/Kconfig
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+source "architecture_common.Kconfig"
diff --git a/scripts/kconfig/kconfirm/tests/arch/x86/Kconfig b/scripts/kconfig/kconfirm/tests/arch/x86/Kconfig
new file mode 100644
index 000000000000..45e94dc6cec6
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/arch/x86/Kconfig
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+source "architecture_common.Kconfig"
diff --git a/scripts/kconfig/kconfirm/tests/architecture.Kconfig b/scripts/kconfig/kconfirm/tests/architecture.Kconfig
new file mode 100644
index 000000000000..a4f1c68548b4
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/architecture.Kconfig
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+source "arch/$(SRCARCH)/Kconfig"
diff --git a/scripts/kconfig/kconfirm/tests/architecture_common.Kconfig b/scripts/kconfig/kconfirm/tests/architecture_common.Kconfig
new file mode 100644
index 000000000000..3205d0d59071
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/architecture_common.Kconfig
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+config SHARED_SELECTEE
+ bool "Shared target"
+ help
+ This is a test-only symbol with a visible prompt.
+ Kconfirm uses it as the target of a select statement.
+ The architecture tests verify that selecting this symbol
+ produces the expected select-visible diagnostic.
+
+config SHARED_SELECTOR
+ bool
+ select SHARED_SELECTEE
+
+config SHARED_DEPENDENCY
+ bool
+ depends on SHARED_PREREQUISITE
+ depends on SHARED_PREREQUISITE
diff --git a/scripts/kconfig/kconfirm/tests/conditional_prompt.Kconfig b/scripts/kconfig/kconfirm/tests/conditional_prompt.Kconfig
new file mode 100644
index 000000000000..f2fef6ea874b
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/conditional_prompt.Kconfig
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+config SELECTEE
+ bool "Target" if PROMPT_CONDITION
+ help
+ This is a test-only symbol with a conditional prompt.
+ Kconfirm uses it as the target of a select statement.
+ The test verifies that the prompt condition is included
+ when deciding whether the target is visible.
+
+config SELECTOR
+ bool
+ select SELECTEE
+
+config PROMPT_CONDITION
+ bool
diff --git a/scripts/kconfig/kconfirm/tests/conftest.py b/scripts/kconfig/kconfirm/tests/conftest.py
new file mode 100644
index 000000000000..005681e7e337
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/conftest.py
@@ -0,0 +1,93 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+"""Fixtures for kconfirm regression tests."""
+
+import os
+import subprocess
+from pathlib import Path
+
+import pytest
+
+
+class Kconfirm:
+ """Test kconfirm against Kconfig fixtures."""
+
+ def __init__(self, binary):
+ self._binary = Path(binary).resolve()
+ self._test_dir = Path(__file__).resolve().parent
+
+ def run_cli(self, arguments, *, arch="x86", environment=None,
+ should_succeed=True):
+ """Run kconfirm with an explicit argument list."""
+ command = [str(self._binary), *arguments]
+
+ process_environment = os.environ.copy()
+ process_environment.pop("SRCARCH", None)
+ process_environment.pop("HEADER_ARCH", None)
+ if arch is None:
+ process_environment.pop("ARCH", None)
+ else:
+ process_environment["ARCH"] = arch
+ if environment:
+ process_environment.update(environment)
+
+ process = subprocess.run(
+ command,
+ capture_output=True,
+ cwd=self._test_dir,
+ env=process_environment,
+ check=False,
+ text=True,
+ )
+
+ print("[command]\n{}\n".format(" ".join(command)))
+ print("[retcode]\n{}\n".format(process.returncode))
+ print("[stdout]\n{}".format(process.stdout))
+ print("[stderr]\n{}".format(process.stderr))
+
+ if should_succeed:
+ assert process.returncode == 0
+ else:
+ assert process.returncode != 0
+
+ return process
+
+ def run(self, kconfig, *, arch="x86", enable_check=None,
+ disable_check=None, environment=None, should_succeed=True):
+ """Run kconfirm against one Kconfig fixture."""
+ arguments = [
+ "--linux-path",
+ str(self._test_dir),
+ "--kconfig",
+ kconfig,
+ ]
+
+ if enable_check:
+ arguments.extend(["--enable-check", enable_check])
+ if disable_check:
+ arguments.extend(["--disable-check", disable_check])
+
+ return self.run_cli(
+ arguments,
+ arch=arch,
+ environment=environment,
+ should_succeed=should_succeed,
+ )
+
+
+def pytest_addoption(parser):
+ parser.addoption(
+ "--kconfirm",
+ metavar="PATH",
+ help="path to the kconfirm executable",
+ )
+
+
+@pytest.fixture(scope="session")
+def kconfirm(request):
+ binary = request.config.getoption("--kconfirm")
+ if not binary:
+ raise pytest.UsageError("--kconfirm is required")
+
+ return Kconfirm(binary)
diff --git a/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_1.Kconfig b/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_1.Kconfig
new file mode 100644
index 000000000000..3e3cc1c72ecf
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_1.Kconfig
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+config A
+ tristate
+ depends on !(B || C)
+ default y if B || C
+
+config B
+ bool
+
+config C
+ tristate
diff --git a/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_2.Kconfig b/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_2.Kconfig
new file mode 100644
index 000000000000..64a1778b2981
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_2.Kconfig
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+config A
+ tristate
+ depends on B || C
+ default y if !(B || C)
+
+config B
+ bool
+
+config C
+ tristate
diff --git a/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_1.Kconfig b/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_1.Kconfig
new file mode 100644
index 000000000000..34afd2a48396
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_1.Kconfig
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+config A
+ tristate
+ depends on B
+ default y if !B
+
+config B
+ bool
+
+config C
+ tristate
diff --git a/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_2.Kconfig b/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_2.Kconfig
new file mode 100644
index 000000000000..d578b130faab
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_2.Kconfig
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+config A
+ tristate
+ depends on !B
+ default y if B
+
+config B
+ bool
+
+config C
+ tristate
diff --git a/scripts/kconfig/kconfirm/tests/dead_link.Kconfig b/scripts/kconfig/kconfirm/tests/dead_link.Kconfig
new file mode 100644
index 000000000000..29bce7aa30fc
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/dead_link.Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+config HAS_DEAD_LINK
+ bool
+ help
+ The obsolete documentation was at https://example.invalid/obsolete.
+
+config HAS_DUPLICATE_DEAD_LINK
+ bool
+ help
+ The same documentation was at https://example.invalid/obsolete.
diff --git a/scripts/kconfig/kconfirm/tests/default_categorization.Kconfig b/scripts/kconfig/kconfirm/tests/default_categorization.Kconfig
new file mode 100644
index 000000000000..45ff769ad1c8
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/default_categorization.Kconfig
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+config DUPLICATE_UNCONDITIONAL
+ bool
+ default y
+ default y
+
+config DEAD_AFTER_UNCONDITIONAL
+ bool
+ default y
+ default n
+
+config DUPLICATE_CONDITIONAL
+ bool
+ default y if CONDITION
+ default y if CONDITION
+
+config CONDITION
+ bool
diff --git a/scripts/kconfig/kconfirm/tests/pytest.ini b/scripts/kconfig/kconfirm/tests/pytest.ini
new file mode 100644
index 000000000000..2e24f7bc9f2c
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+addopts = --verbose
diff --git a/scripts/kconfig/kconfirm/tests/ranges.Kconfig b/scripts/kconfig/kconfirm/tests/ranges.Kconfig
new file mode 100644
index 000000000000..4ced953247e1
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/ranges.Kconfig
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+#
+# Fixture for toggling duplicate_range and dead_range independently of each
+# other, and for categorizing repeated ranges correctly.
+
+config GATE
+ bool
+
+config OTHER_GATE
+ bool
+
+config RANGED_DUP
+ int
+ range 1 5
+ range 1 5
+
+config RANGED_DEAD
+ int
+ range 1 5
+ range 2 6 if GATE
+
+config RANGED_CONDITIONAL_FIRST
+ int
+ range 1 5 if GATE
+ range 2 6
+
+config RANGED_DEAD_UNCONDITIONAL
+ int
+ range 1 5
+ range 2 6
+
+config RANGED_MULTIPLE
+ int
+ range 1 5 if GATE
+ range 2 6 if OTHER_GATE
+ range 3 7
+ range 4 8 if GATE
+ range 5 9
diff --git a/scripts/kconfig/kconfirm/tests/select_imply.Kconfig b/scripts/kconfig/kconfirm/tests/select_imply.Kconfig
new file mode 100644
index 000000000000..ac8f2aa1b66c
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/select_imply.Kconfig
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+#
+# Fixture for toggling duplicate_select/dead_select and
+# duplicate_imply/dead_imply independently of each other.
+
+config SELECTEE
+ bool
+
+config OTHER_SELECTEE
+ bool
+
+config GATE
+ bool
+
+config SELECTOR
+ bool
+ select SELECTEE
+ select SELECTEE if GATE
+ select OTHER_SELECTEE
+ select OTHER_SELECTEE
+
+config IMPLIER
+ bool
+ imply SELECTEE
+ imply SELECTEE if GATE
+ imply OTHER_SELECTEE
+ imply OTHER_SELECTEE
diff --git a/scripts/kconfig/kconfirm/tests/test_kconfirm.py b/scripts/kconfig/kconfirm/tests/test_kconfirm.py
new file mode 100644
index 000000000000..5e23f7d2b0cb
--- /dev/null
+++ b/scripts/kconfig/kconfirm/tests/test_kconfirm.py
@@ -0,0 +1,358 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+"""Regression tests for kconfirm."""
+
+import shutil
+from pathlib import Path
+
+import pytest
+
+
+TEST_DIR = Path(__file__).resolve().parent
+
+
+@pytest.mark.parametrize(
+ "fixture",
+ [
+ "default_constant_condition_negative_expression_1.Kconfig",
+ "default_constant_condition_negative_expression_2.Kconfig",
+ "default_constant_condition_negative_symbol_1.Kconfig",
+ "default_constant_condition_negative_symbol_2.Kconfig",
+ ],
+)
+def test_negative_constant_condition(kconfirm, fixture):
+ process = kconfirm.run("constant_condition/{}".format(fixture))
+
+ assert "[constant_condition] [X86] config A:" in process.stdout
+ assert "will always be false" in process.stdout
+
+
+def test_default_categorization(kconfirm):
+ process = kconfirm.run("default_categorization.Kconfig")
+
+ assert (
+ "[duplicate_default] [X86] config DUPLICATE_UNCONDITIONAL: "
+ "duplicate default of y"
+ ) in process.stdout
+ assert (
+ "[dead_default] [X86] config DUPLICATE_UNCONDITIONAL:"
+ ) not in process.stdout
+ assert (
+ "[dead_default] [X86] config DEAD_AFTER_UNCONDITIONAL: "
+ "dead default of n"
+ ) in process.stdout
+ assert (
+ "[duplicate_default] [X86] config DUPLICATE_CONDITIONAL: "
+ "duplicate default of y"
+ ) in process.stdout
+
+
+def test_select_and_imply_checks(kconfirm):
+ process = kconfirm.run("select_imply.Kconfig")
+
+ assert (
+ "[dead_select] [X86] config SELECTOR: dead select of SELECTEE"
+ ) in process.stdout
+ assert (
+ "[dead_imply] [X86] config IMPLIER: dead imply of SELECTEE"
+ ) in process.stdout
+
+
+def test_disable_dead_select_and_imply_checks(kconfirm):
+ process = kconfirm.run(
+ "select_imply.Kconfig",
+ disable_check="dead_select,dead_imply",
+ )
+
+ assert "[dead_select]" not in process.stdout
+ assert "[dead_imply]" not in process.stdout
+ assert (
+ "[duplicate_select] [X86] config SELECTOR: "
+ "duplicate select of OTHER_SELECTEE"
+ ) in process.stdout
+ assert (
+ "[duplicate_imply] [X86] config IMPLIER: "
+ "duplicate imply of OTHER_SELECTEE"
+ ) in process.stdout
+
+
+def test_disable_duplicate_select_and_imply_checks(kconfirm):
+ process = kconfirm.run(
+ "select_imply.Kconfig",
+ disable_check="duplicate_select,duplicate_imply",
+ )
+
+ assert "[duplicate_select]" not in process.stdout
+ assert "[duplicate_imply]" not in process.stdout
+ assert (
+ "[dead_select] [X86] config SELECTOR: dead select of SELECTEE"
+ ) in process.stdout
+ assert (
+ "[dead_imply] [X86] config IMPLIER: dead imply of SELECTEE"
+ ) in process.stdout
+
+
+def test_range_checks(kconfirm):
+ process = kconfirm.run("ranges.Kconfig")
+
+ assert (
+ "[duplicate_range] [X86] config RANGED_DUP: duplicate range 1 5"
+ ) in process.stdout
+ assert "[dead_range] [X86] config RANGED_DUP:" not in process.stdout
+ assert (
+ "[dead_range] [X86] config RANGED_DEAD: dead range of 2 6"
+ ) in process.stdout
+ assert (
+ "[dead_range] [X86] config RANGED_DEAD_UNCONDITIONAL: "
+ "dead range of 2 6"
+ ) in process.stdout
+ assert (
+ "[dead_range] [X86] config RANGED_CONDITIONAL_FIRST:"
+ ) not in process.stdout
+ assert process.stdout.count(
+ "[dead_range] [X86] config RANGED_MULTIPLE:"
+ ) == 2
+
+
+def test_disable_dead_range_check(kconfirm):
+ process = kconfirm.run(
+ "ranges.Kconfig",
+ disable_check="dead_range",
+ )
+
+ assert "[dead_range]" not in process.stdout
+ assert (
+ "[duplicate_range] [X86] config RANGED_DUP: duplicate range 1 5"
+ ) in process.stdout
+
+
+def test_disable_duplicate_range_check(kconfirm):
+ process = kconfirm.run(
+ "ranges.Kconfig",
+ disable_check="duplicate_range",
+ )
+
+ assert "[duplicate_range]" not in process.stdout
+ assert (
+ "[dead_range] [X86] config RANGED_DEAD: dead range of 2 6"
+ ) in process.stdout
+
+
+def test_architecture_directory_is_discovered(kconfirm):
+ process = kconfirm.run(
+ "default_categorization.Kconfig",
+ arch="testarch",
+ )
+
+ assert (
+ "[duplicate_default] [TESTARCH] config DUPLICATE_UNCONDITIONAL:"
+ ) in process.stdout
+
+
+def test_unknown_architecture_is_rejected(kconfirm):
+ process = kconfirm.run(
+ "conditional_prompt.Kconfig",
+ arch="missing",
+ should_succeed=False,
+ )
+ output = process.stdout + process.stderr
+
+ assert "unexpected architecture from ARCH" in output
+ for architecture in ["arm", "powerpc", "riscv", "sh", "testarch", "um", "x86"]:
+ assert architecture in output
+
+
+def test_missing_architecture_is_rejected(kconfirm):
+ process = kconfirm.run(
+ "conditional_prompt.Kconfig",
+ arch=None,
+ should_succeed=False,
+ )
+
+ assert "ARCH environment variable is required" in process.stderr
+
+
+def test_conditional_prompt_is_not_select_visible(kconfirm):
+ process = kconfirm.run(
+ "conditional_prompt.Kconfig",
+ enable_check="select_visible",
+ )
+
+ assert "[select_visible]" not in process.stdout
+
+
+@pytest.mark.parametrize(
+ ("arch", "arch_config", "specific_symbol", "specific_dependency"),
+ [
+ ("arm", "ARM", "ARM_ONLY", "ARM_PREREQUISITE"),
+ ("riscv", "RISCV", "RISCV_ONLY", "RISCV_PREREQUISITE"),
+ ],
+)
+def test_architecture_findings(
+ kconfirm,
+ arch,
+ arch_config,
+ specific_symbol,
+ specific_dependency,
+):
+ process = kconfirm.run(
+ "architecture.Kconfig",
+ arch=arch,
+ enable_check="select_visible",
+ )
+
+ assert (
+ f"[duplicate_dependency] [{arch_config}] config SHARED_DEPENDENCY: "
+ "duplicate dependency on SHARED_PREREQUISITE"
+ ) in process.stdout
+ assert (
+ f"[select_visible] [{arch_config}] config SHARED_SELECTOR: "
+ "selects the visible SHARED_SELECTEE"
+ ) in process.stdout
+ assert (
+ f"[duplicate_dependency] [{arch_config}] config {specific_symbol}: "
+ f"duplicate dependency on {specific_dependency}"
+ ) in process.stdout
+
+
+@pytest.mark.parametrize(
+ ("arch", "arch_config"),
+ [
+ ("x86_64", "X86"),
+ ("powerpc", "PPC"),
+ ("sh", "SUPERH"),
+ ("um", "UML"),
+ ],
+)
+def test_architecture_alias_selects_source_directory(kconfirm, arch, arch_config):
+ process = kconfirm.run(
+ "architecture.Kconfig",
+ arch=arch,
+ enable_check="select_visible",
+ )
+
+ assert (
+ f"[duplicate_dependency] [{arch_config}] config SHARED_DEPENDENCY: "
+ "duplicate dependency on SHARED_PREREQUISITE"
+ ) in process.stdout
+
+
+@pytest.mark.skipif(shutil.which("curl") is None, reason="requires curl")
+def test_dead_links_are_deduplicated(kconfirm):
+ # Both fixture config options point at the same example.invalid URL. That
+ # TLD is reserved and never resolves (RFC 6761), so curl should fail
+ # deterministically, and the link reported once. The second config option
+ # referencing it should be deduplicated.
+ process = kconfirm.run(
+ "dead_link.Kconfig",
+ enable_check="dead_link",
+ )
+
+ assert (
+ "[dead_link] [X86] config HAS_DEAD_LINK:"
+ ) in process.stdout
+ assert (
+ "[dead_link] [X86] config HAS_DUPLICATE_DEAD_LINK:"
+ ) not in process.stdout
+
+
+def test_dead_link_requires_curl(kconfirm):
+ process = kconfirm.run(
+ "dead_link.Kconfig",
+ enable_check="dead_link",
+ environment={"PATH": ""},
+ should_succeed=False,
+ )
+
+ assert "dead_link requires the `curl` command" in process.stderr
+
+
+@pytest.mark.parametrize(
+ "arguments",
+ [
+ [
+ f"--linux-path={TEST_DIR}",
+ "--kconfig=conditional_prompt.Kconfig",
+ "--enable-check=select_visible",
+ "--disable-check=dead_range",
+ ],
+ [
+ f"-l{TEST_DIR}",
+ "-kconditional_prompt.Kconfig",
+ "-eselect_visible",
+ "-ddead_range",
+ ],
+ ],
+)
+def test_attached_option_values(kconfirm, arguments):
+ process = kconfirm.run_cli(arguments)
+
+ assert "[select_visible]" not in process.stdout
+
+
+def test_repeated_check_options(kconfirm):
+ process = kconfirm.run_cli(
+ [
+ "--linux-path",
+ str(TEST_DIR),
+ "--kconfig",
+ "conditional_prompt.Kconfig",
+ "--enable-check",
+ "select_visible",
+ "--enable-check",
+ "duplicate_default_value",
+ "--disable-check",
+ "dead_range",
+ "--disable-check",
+ "reverse_range",
+ ],
+ )
+
+ assert process.stderr == ""
+
+
+def test_help(kconfirm):
+ process = kconfirm.run_cli(["--help"])
+
+ assert "Usage: kconfirm --linux-path PATH" in process.stdout
+
+
+@pytest.mark.parametrize(
+ ("arguments", "message"),
+ [
+ ([], "--linux-path is required"),
+ (["--unknown"], "unrecognized option '--unknown'"),
+ (
+ [
+ "--linux-path",
+ str(TEST_DIR),
+ "--enable-check",
+ "not_a_check",
+ ],
+ "check not_a_check does not exist",
+ ),
+ (
+ [
+ "--linux-path",
+ str(TEST_DIR),
+ "--enable-check",
+ "ungrouped_attribute",
+ ],
+ "check ungrouped_attribute does not exist",
+ ),
+ ],
+)
+def test_cli_errors(kconfirm, arguments, message):
+ process = kconfirm.run_cli(arguments, should_succeed=False)
+
+ assert message in process.stdout + process.stderr
+
+
+def test_invalid_linux_path(kconfirm):
+ process = kconfirm.run_cli(
+ ["--linux-path", str(TEST_DIR / "does-not-exist")],
+ should_succeed=False,
+ )
+
+ assert "No such file or directory" in process.stderr
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v4 4/5] Documentation: add kconfirm
2026-07-27 0:16 [PATCH v4 0/5] add kconfirm Julian Braha
` (2 preceding siblings ...)
2026-07-27 0:16 ` [PATCH v4 3/5] kconfirm: add tests Julian Braha
@ 2026-07-27 0:16 ` Julian Braha
2026-07-27 0:16 ` [PATCH v4 5/5] MAINTAINERS: add entry for kconfirm Julian Braha
2026-09-04 12:29 ` [PATCH v4 0/5] add kconfirm Julian Braha
5 siblings, 0 replies; 10+ messages in thread
From: Julian Braha @ 2026-07-27 0:16 UTC (permalink / raw)
To: nathan, nsc
Cc: ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs, andrew.jones,
masahiroy, corbet, qingfang.deng, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
Add usage documentation and a brief description of kconfirm to
Documentation/dev-tools/
Signed-off-by: Julian Braha <julianbraha@gmail.com>
---
Documentation/dev-tools/index.rst | 1 +
Documentation/dev-tools/kconfirm.rst | 229 +++++++++++++++++++++++++++
2 files changed, 230 insertions(+)
create mode 100644 Documentation/dev-tools/kconfirm.rst
diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst
index 59cbb77b33ff..130ebc0d7282 100644
--- a/Documentation/dev-tools/index.rst
+++ b/Documentation/dev-tools/index.rst
@@ -40,3 +40,4 @@ Documentation/process/debugging/index.rst
autofdo
propeller
container
+ kconfirm
diff --git a/Documentation/dev-tools/kconfirm.rst b/Documentation/dev-tools/kconfirm.rst
new file mode 100644
index 000000000000..64ab9d1c3057
--- /dev/null
+++ b/Documentation/dev-tools/kconfirm.rst
@@ -0,0 +1,229 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+.. Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+========
+kconfirm
+========
+
+kconfirm is a static analysis tool for the kernel's Kconfig. It checks
+the entire tree-wide Kconfig, and reports misusage like dead code. In the
+case of dead default statements, these can be a code smell.
+
+kconfirm has some additional, optional checks. The first is for dead links
+in the Kconfig help texts. Since this has a high potential for false
+positives (due to websites blocking bots) and slows down runtime
+significantly, it is disabled by default.
+
+Another optional check is for config options that select visible config
+options. Examples of how to enable the optional checks are included
+below.
+
+kconfirm is written in Rust and lives in ``scripts/kconfig/kconfirm``. Other
+than the dead link checks, kconfirm aims for zero false positives, though some
+will necessarily happen for config options that use macros referencing the host
+environment. These are common for host compiler-related options.
+
+kconfirm checks one architecture per run. When run with ``make kconfirm``, it
+checks the same architecture as the kernel build. That is, it reads the
+``ARCH`` environment variable, similarly to the build system. Findings include
+the source architecture Kconfig option as a tag; for example, ``[RISCV]``
+indicates a finding from a tree that sourced ``arch/riscv/Kconfig``.
+
+**NOTE**: kconfirm does not build the kernel; it is strictly a static checker.
+Also note that parsing Kconfig runs Kconfig's own ``$(shell,...)`` and
+``$(success,...)`` feature probes, just as ``make menuconfig`` does, so some
+scripts under ``scripts/`` are executed and your compiler is queried along the
+way.
+
+
+Getting Started
+===============
+
+
+Beyond the usual kernel build environment, kconfirm needs the Rust toolchain:
+``rustc`` and ``bindgen`` (which in turn uses libclang). See also
+Documentation/rust/quick-start.rst for how to install and set it up, and
+Documentation/process/changes.rst for the minimum versions. kconfirm's
+Minimum Supported Rust Version follows the kernel's host-Rust toolchain
+requirement.
+
+kconfirm is built directly by Kbuild with ``rustc`` and has no third-party
+Rust dependencies. Bindgen generates the raw Rust bindings directly from the
+Kconfig parser's headers in ``scripts/kconfig``; the generated file is kept in
+the Kbuild output tree. ``make kconfirm`` verifies that ``rustc`` and
+``bindgen`` are available and recent enough before building, and exits with
+guidance when they are not.
+
+The optional ``dead_link`` check verifies HTTP and HTTPS links and requires the
+``curl`` command at runtime.
+Attempting to enable ``dead_link`` without ``curl`` available in ``PATH`` exits
+with an error. An internet connection is only required when this check is run.
+
+kconfirm can be built and run from the top of the kernel source tree::
+
+ make kconfirm
+
+The compiled binary will be available at
+``scripts/kconfig/kconfirm/kconfirm`` for an in-tree build, or under the
+corresponding ``scripts/kconfig/kconfirm`` directory in the Kbuild output
+tree when using ``O=``.
+
+Run the kconfirm tests with::
+
+ make kconfirmtest
+
+Run the tests with the kernel's Rust lint configuration with::
+
+ make CLIPPY=1 kconfirmtest
+
+The default checks currently cover dead code analysis, as well as invalid
+(reverse) ranges and constant conditions. ``select_visible`` and
+``dead_link`` must be turned on explicitly with ``--enable-check``;
+conversely, any default check can be turned off with ``--disable-check``. Both
+options accept either a comma-separated list or repeated flags, so the
+following two invocations are equivalent::
+
+ make ARCH=x86 kconfirm KCONFIRM_ARGS="--enable-check select_visible,dead_link"
+ make ARCH=x86 kconfirm KCONFIRM_ARGS="--enable-check select_visible --enable-check dead_link"
+
+
+Command-line options
+====================
+
+**NOTE**: kconfirm's arguments must be provided in the ``KCONFIRM_ARGS`` make
+variable. See `Examples`_.
+
+Every option below also has a single-letter form, and accepts its value
+either as the next argument or attached with ``=``, so
+``--enable-check dead_link``, ``--enable-check=dead_link`` and
+``-e dead_link`` are all equivalent.
+
+Available options:
+
+``-l, --linux-path PATH``
+
+ The path to the linux source tree to analyze. Required. ``make`` uses
+ this internal option to pass the current linux tree.
+
+``-e, --enable-check CHECK[,CHECK...]``
+
+ Enable one or more checks in addition to the default set. May be
+ given multiple times, or as a single comma-separated list. See
+ `Available checks`_ below for valid names.
+
+``-d, --disable-check CHECK[,CHECK...]``
+
+ Disable one or more checks from the default set. May be given
+ multiple times, or as a single comma-separated list.
+
+``-k, --kconfig FILE``
+
+ The top-level Kconfig file to start from, relative to ``--linux-path``.
+ Defaults to ``Kconfig``. ``make`` passes the same file that the other
+ Kconfig targets use, so ``KBUILD_KCONFIG`` is honoured.
+
+``-h, --help``
+
+ Show the help message and exit.
+
+
+Available checks
+================
+
+Each check has a string name that is accepted by ``--enable-check`` and
+``--disable-check``. Checks marked *(default)* are enabled unless turned
+off explicitly.
+
+``duplicate_dependency`` *(default)*
+
+ Reports duplicated ``depends on`` entries on a single Kconfig symbol.
+
+``duplicate_range`` *(default)*
+
+ Reports duplicated ``range`` entries on a single Kconfig symbol.
+
+``dead_range`` *(default)*
+
+ Reports ``range`` entries that will never be evaluated, due to an
+ unconditional range entry.
+
+``duplicate_select`` *(default)*
+
+ Reports duplicated ``select`` entries on a single Kconfig symbol.
+
+``dead_select`` *(default)*
+
+ Reports dead ``select`` entries that will never be evaluated, due to an
+ unconditional select entry of the same config option.
+
+``duplicate_imply`` *(default)*
+
+ Reports duplicated ``imply`` entries on a single Kconfig symbol.
+
+``dead_imply`` *(default)*
+
+ Reports dead ``imply`` entries that will never be evaluated, due to an
+ unconditional imply entry for the same config option.
+
+``duplicate_default`` *(default)*
+
+ Reports duplicated ``default`` entries on a single Kconfig symbol.
+
+``dead_default`` *(default)*
+
+ Reports ``default`` entries that can never be selected because an earlier
+ unconditional default or a default with the same condition takes
+ precedence.
+
+``constant_condition`` *(default)*
+
+ Reports conditions on defaults, selects, implies, and ranges that always
+ evaluate to ``true`` or ``false`` because the condition, or its negation,
+ is already a dependency.
+
+``reverse_range`` *(default)*
+
+ Reports invalid ranges for int and hex configuration options.
+
+``select_visible``
+
+ Reports configuration options that ``select`` a config option that is
+ visible to users.
+
+``dead_link``
+
+ Reports broken HTTP and HTTPS URLs found in Kconfig help text. Because this
+ performs network requests it can be quite slow, and is disabled by
+ default. May also have false positives.
+
+``duplicate_default_value``
+
+ Reports duplicate default values that have different conditions.
+ Suggests combining the conditions using a logical-or ``||``.
+ This is a style check, and is disabled by default.
+
+
+Examples
+========
+
+Compile (as needed) and run on the current tree::
+
+ make kconfirm
+
+To additionally enable the dead link and select-visible checks::
+
+ make kconfirm KCONFIRM_ARGS="--enable-check=dead_link,select_visible"
+
+To disable a check (here, ``duplicate_dependency``) while keeping the
+rest of the default set::
+
+ make kconfirm KCONFIRM_ARGS="--disable-check duplicate_dependency"
+
+To check another architecture, such as RISC-V::
+
+ make ARCH=riscv kconfirm
+
+To run the default checks from a kernel tree separate from the current
+directory, such as ``~/repos/linux``::
+
+ make -C ~/repos/linux ARCH=x86 kconfirm
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v4 5/5] MAINTAINERS: add entry for kconfirm
2026-07-27 0:16 [PATCH v4 0/5] add kconfirm Julian Braha
` (3 preceding siblings ...)
2026-07-27 0:16 ` [PATCH v4 4/5] Documentation: add kconfirm Julian Braha
@ 2026-07-27 0:16 ` Julian Braha
2026-09-04 12:29 ` [PATCH v4 0/5] add kconfirm Julian Braha
5 siblings, 0 replies; 10+ messages in thread
From: Julian Braha @ 2026-07-27 0:16 UTC (permalink / raw)
To: nathan, nsc
Cc: ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs, andrew.jones,
masahiroy, corbet, qingfang.deng, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
I will maintain all files introduced with kconfirm.
Signed-off-by: Julian Braha <julianbraha@gmail.com>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index cc3c01eecfa2..c7066ccef6c9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14041,6 +14041,13 @@ F: Documentation/kbuild/kconfig*
F: scripts/Kconfig.include
F: scripts/kconfig/
+KCONFIRM AND KCONFIG BINDINGS [RUST]
+M: Julian Braha <julianbraha@gmail.com>
+S: Maintained
+F: Documentation/dev-tools/kconfirm.rst
+F: scripts/kconfig/kconfig.rs
+F: scripts/kconfig/kconfirm/
+
KCORE
M: Omar Sandoval <osandov@osandov.com>
L: linux-debuggers@vger.kernel.org
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH v4 0/5] add kconfirm
2026-07-27 0:16 [PATCH v4 0/5] add kconfirm Julian Braha
` (4 preceding siblings ...)
2026-07-27 0:16 ` [PATCH v4 5/5] MAINTAINERS: add entry for kconfirm Julian Braha
@ 2026-09-04 12:29 ` Julian Braha
2026-09-04 18:37 ` Nicolas Schier
2026-09-04 22:05 ` Nathan Chancellor
5 siblings, 2 replies; 10+ messages in thread
From: Julian Braha @ 2026-09-04 12:29 UTC (permalink / raw)
To: nathan, nsc
Cc: ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs, andrew.jones,
masahiroy, corbet, qingfang.deng, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild
On 7/27/26 01:16, Julian Braha wrote:
> kconfirm now uses the in-tree parser. Making this migration required a
> modification to the parser that allows us to observe the parse tree before
> its final simplification step, thus allowing us to detect dead code.
>
> Since no external crates are now necessary, I’ve removed the Cargo requirement,
> too.
>
> I believe these changes should resolve the major questions, so I’ve
> removed the RFC tag.
>
> Now, onto the existing patch-set description since the RFCs:
>
> ===
>
> kconfirm is a tool to detect misusage of Kconfig. It detects dead code,
> constant conditions, and invalid (reverse) ranges. There are also optional
> checks to detect config options that select visible config options, and to
> check for dead links in the help texts.
>
> See also kconfirm's original introduction to the mailing list:
> https://lore.kernel.org/all/6ec4df6d-1445-48ca-8f54-1d1a83c4716d@gmail.com/
>
> False Alarms:
> kconfirm aims for zero false-positives, though this is not completely
> feasible due to macro evaluation from the host environment, primarily
> affecting host compiler-related options. There will also be some false
> positives for dead link checks, as this depends on an internet connection,
> and we do not attempt to bypass bot blocks. For this reason, dead link
> checking is disabled by default, but I've provided an example below of how
> to enable it. Additionally, you can view my previous message to the
> mailing list with hand-verified dead links here:
> https://lore.kernel.org/all/6732bf08-41ee-40c4-83b2-4ae8bc0da7cf@gmail.com/
>
> Additionally, there is an optional check to detect config options that
> select visible config options, as requested by Jani during the review of
> the first RFC:
> https://lore.kernel.org/all/dcb7439832f0bb35598fba653d922b5f6a4d0058@intel.com/
>
> Even after deduplicating across architectures, there are well over 1,000
> instances of these select-visible cases, and I suspect that, despite the
> Kconfig documentation saying select-visible should be avoided, some
> exceptions will be made. So, I have left this check disabled by default,
> keeping in line with the goal of having a low-noise checker. If interested
> in using it, I have included an example below of how to enable this check.
>
> Current State of Alarms:
> With x86-64 on Linux v7.2-rc4 (which this RFC is based), there are 1282
> alarms coming from the default set of checks, and an additional 976 alarms
> if enabling the optional select-visible check. The last time I checked
> linux-next (next-20260427), there were 81 unique dead links.
>
> The most critical check is the dead default statements, which has surfaced
> a few misconfiguration bugs (fortunately, just for kunit tests), see
> examples:
> https://lore.kernel.org/all/20260323124118.1414913-1-julianbraha@gmail.com/
> and:
> https://lore.kernel.org/all/20260323123536.1413732-1-julianbraha@gmail.com/
>
> But hopefully kconfirm can ease maintenance and we can prevent more of
> these from making it into the tree in the future.
>
> Use it:
> You can test out kconfirm with this patch series by compiling and running
> kconfirm like this:
>
> `make kconfirm`
>
> To enable the select-visible check:
> `KCONFIRM_ARGS="--enable-check select_visible" make kconfirm`
>
> And to enable dead link checks in the help texts:
> `KCONFIRM_ARGS="--enable-check dead_link" make kconfirm`
>
> kconfirm by default runs on the same architecture as the kernel build
> would. To run kconfirm on another architecture (for example, ARM with an
> X86 host):
> `ARCH=arm make kconfirm`
>
> Thanks,
> Julian Braha
> ---
> Changes since RFC v3:
> - Modify kconfig parser to make raw parse tree viewable to enable analysis
> - Switch from external parser to in-tree kconfig parser (Demi)
> - Add Rust bindings for kconfig
> - Remove Cargo and external crates
> - Make curl an optional dependency for optional dead link check (Arnd)
> - Switch from libcurl to curl CLI for dead link checks (Miguel)
> - Adhere to Rust-for-Linux style (Miguel)
> - Add tests (Miguel)
> - Move kconfirm under scripts/kconfig/ to resolve broken tab autocompletion (Nathan)
> - Remove ungrouped attributes style check
> - Add support for checking liveness of ftp and git URIs in help texts
> - Dropped RFC tag
>
> Link to RFC v3:
> https://lore.kernel.org/all/20260516215354.449807-1-julianbraha@gmail.com/
>
> Changes since RFC v2:
> - Reduce Rust dependencies significantly (follows Demi's suggestions):
> - from 6 direct dependencies to 1
> - from 107 indirect dependencies to 4
> - Replace ureq crate with usage of system libcurl (thanks Demi)
> - Replace clap crate with FFI bindings to libc's getopt_long (also Demi)
> - Remove crates env_logger, regex
> - Switch from vendoring dependencies to requiring users to first download
> outside of Make (as suggested by Miguel)
> - Various makefile improvements (as pointed out by Nicolas):
> - Fix out-of-tree builds
> - Only delete kconfirm artifacts with 'distclean' and 'mrproper'
> - Add myself as maintainer of kconfirm (as discussed with Nicolas)
> - Remove dedicated code license file (pointed out by Jani)
> - Update documentation to explain tool setup
> - Add hint to users to check documentation and download tool dependencies
> - Address sashiko's many code-level and documentation suggestions:
> - Follow the kernel's rust import style
> - Fix a dead_range/duplicate_range alarm mixup
> - Fix potential duplicates in default value style check
> - Avoid panicking on errors
> - Clarify parse failure check usage in documentation
> - Fix typo in documentation
> - Can now enable architectures and disable the default (host) architecture in the CLI
>
> Link to RFC v2:
> https://lore.kernel.org/all/20260509203808.1142311-1-julianbraha@gmail.com/
>
> Changes since RFC v1:
> - vendored dependencies instead of requiring an internet connection
> - removed Cargo.lock
> - replaced reqwest dependency with smaller ureq
> - removed rustls, expect user to have openssl instead
> - added select-visible check based on Jani's feature request
> - added invalid (reverse) range check
> - deduplicating alarms that appear for multiple architectures
> - `make clean` no longer deletes kconfirm's build artifacts
> - typo fixes in documentation
> - added patch description for the main "add kconfirm" patch (patch 1/2)
>
> Link to RFC v1:
> https://lore.kernel.org/all/20260427174429.779474-1-julianbraha@gmail.com/
> ---
>
> Julian Braha (5):
> kconfig: add add another callback to the parser to view raw parse tree
> kconfig: add kconfirm
> kconfirm: add tests
> Documentation: add kconfirm
> MAINTAINERS: add entry for kconfirm
>
> Documentation/dev-tools/index.rst | 1 +
> Documentation/dev-tools/kconfirm.rst | 229 ++++++
> MAINTAINERS | 7 +
> Makefile | 17 +-
> scripts/kconfig/.gitignore | 1 +
> scripts/kconfig/Makefile | 54 ++
> scripts/kconfig/kconfig.rs | 445 +++++++++++
> scripts/kconfig/kconfirm/.gitignore | 2 +
> scripts/kconfig/kconfirm/analyze.rs | 340 ++++++++
> scripts/kconfig/kconfirm/arch.rs | 53 ++
> scripts/kconfig/kconfirm/checks.rs | 748 ++++++++++++++++++
> scripts/kconfig/kconfirm/dead_links.rs | 230 ++++++
> scripts/kconfig/kconfirm/kconfirm-cfg.sh | 57 ++
> scripts/kconfig/kconfirm/kconfirm.rs | 278 +++++++
> scripts/kconfig/kconfirm/output.rs | 87 ++
> scripts/kconfig/kconfirm/symbol_table.rs | 105 +++
> .../kconfig/kconfirm/tests/arch/arm/Kconfig | 9 +
> .../kconfirm/tests/arch/powerpc/Kconfig | 4 +
> .../kconfig/kconfirm/tests/arch/riscv/Kconfig | 9 +
> .../kconfig/kconfirm/tests/arch/sh/Kconfig | 4 +
> .../kconfirm/tests/arch/testarch/Kconfig | 4 +
> .../kconfig/kconfirm/tests/arch/um/Kconfig | 4 +
> .../kconfig/kconfirm/tests/arch/x86/Kconfig | 4 +
> .../kconfirm/tests/architecture.Kconfig | 4 +
> .../tests/architecture_common.Kconfig | 19 +
> .../kconfirm/tests/conditional_prompt.Kconfig | 17 +
> scripts/kconfig/kconfirm/tests/conftest.py | 93 +++
> ...nt_condition_negative_expression_1.Kconfig | 13 +
> ...nt_condition_negative_expression_2.Kconfig | 13 +
> ...nstant_condition_negative_symbol_1.Kconfig | 13 +
> ...nstant_condition_negative_symbol_2.Kconfig | 13 +
> .../kconfig/kconfirm/tests/dead_link.Kconfig | 12 +
> .../tests/default_categorization.Kconfig | 20 +
> scripts/kconfig/kconfirm/tests/pytest.ini | 2 +
> scripts/kconfig/kconfirm/tests/ranges.Kconfig | 39 +
> .../kconfirm/tests/select_imply.Kconfig | 28 +
> .../kconfig/kconfirm/tests/test_kconfirm.py | 358 +++++++++
> scripts/kconfig/lkc_proto.h | 2 +
> scripts/kconfig/parser.y | 21 +
> 39 files changed, 3357 insertions(+), 2 deletions(-)
> create mode 100644 Documentation/dev-tools/kconfirm.rst
> create mode 100644 scripts/kconfig/kconfig.rs
> create mode 100644 scripts/kconfig/kconfirm/.gitignore
> create mode 100644 scripts/kconfig/kconfirm/analyze.rs
> create mode 100644 scripts/kconfig/kconfirm/arch.rs
> create mode 100644 scripts/kconfig/kconfirm/checks.rs
> create mode 100644 scripts/kconfig/kconfirm/dead_links.rs
> create mode 100755 scripts/kconfig/kconfirm/kconfirm-cfg.sh
> create mode 100644 scripts/kconfig/kconfirm/kconfirm.rs
> create mode 100644 scripts/kconfig/kconfirm/output.rs
> create mode 100644 scripts/kconfig/kconfirm/symbol_table.rs
> create mode 100644 scripts/kconfig/kconfirm/tests/arch/arm/Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/arch/powerpc/Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/arch/riscv/Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/arch/sh/Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/arch/testarch/Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/arch/um/Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/arch/x86/Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/architecture.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/architecture_common.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/conditional_prompt.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/conftest.py
> create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_1.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_2.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_1.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_2.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/dead_link.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/default_categorization.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/pytest.ini
> create mode 100644 scripts/kconfig/kconfirm/tests/ranges.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/select_imply.Kconfig
> create mode 100644 scripts/kconfig/kconfirm/tests/test_kconfirm.py
>
As I've been getting more comfortable with the in-tree Kconfig
interpreter, I've been wondering if the community might prefer the
gradual integration of lints directly into Kconfig, instead of as a
standalone tool. Similar to a comment by Jani on another patch [1].
Otherwise, I can do a round of self-review and send a v5 for this
series. I already noticed some dumb typos in the subject lines, but any
other feedback on v4 would be welcome if kconfirm is the preferred
route :)
Link:
https://lore.kernel.org/all/f362ddf2e0f3ecf41ed81f03f77577473c3e21bc@intel.com/
[1]
- Julian Braha
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v4 0/5] add kconfirm
2026-09-04 12:29 ` [PATCH v4 0/5] add kconfirm Julian Braha
@ 2026-09-04 18:37 ` Nicolas Schier
2026-09-04 22:05 ` Nathan Chancellor
1 sibling, 0 replies; 10+ messages in thread
From: Nicolas Schier @ 2026-09-04 18:37 UTC (permalink / raw)
To: Julian Braha
Cc: nathan, ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs,
andrew.jones, masahiroy, corbet, qingfang.deng, demiobenour, ej,
linux-kernel, rust-for-linux, linux-doc, linux-kbuild
On Fri, Sep 04, 2026 at 01:29:16PM +0100, Julian Braha wrote:
> On 7/27/26 01:16, Julian Braha wrote:
> > kconfirm now uses the in-tree parser. Making this migration required a
> > modification to the parser that allows us to observe the parse tree before
> > its final simplification step, thus allowing us to detect dead code.
> >
> > Since no external crates are now necessary, I’ve removed the Cargo requirement,
> > too.
> >
> > I believe these changes should resolve the major questions, so I’ve
> > removed the RFC tag.
> >
> > Now, onto the existing patch-set description since the RFCs:
> >
> > ===
> >
> > kconfirm is a tool to detect misusage of Kconfig. It detects dead code,
> > constant conditions, and invalid (reverse) ranges. There are also optional
> > checks to detect config options that select visible config options, and to
> > check for dead links in the help texts.
> >
> > See also kconfirm's original introduction to the mailing list:
> > https://lore.kernel.org/all/6ec4df6d-1445-48ca-8f54-1d1a83c4716d@gmail.com/
> >
> > False Alarms:
> > kconfirm aims for zero false-positives, though this is not completely
> > feasible due to macro evaluation from the host environment, primarily
> > affecting host compiler-related options. There will also be some false
> > positives for dead link checks, as this depends on an internet connection,
> > and we do not attempt to bypass bot blocks. For this reason, dead link
> > checking is disabled by default, but I've provided an example below of how
> > to enable it. Additionally, you can view my previous message to the
> > mailing list with hand-verified dead links here:
> > https://lore.kernel.org/all/6732bf08-41ee-40c4-83b2-4ae8bc0da7cf@gmail.com/
> >
> > Additionally, there is an optional check to detect config options that
> > select visible config options, as requested by Jani during the review of
> > the first RFC:
> > https://lore.kernel.org/all/dcb7439832f0bb35598fba653d922b5f6a4d0058@intel.com/
> >
> > Even after deduplicating across architectures, there are well over 1,000
> > instances of these select-visible cases, and I suspect that, despite the
> > Kconfig documentation saying select-visible should be avoided, some
> > exceptions will be made. So, I have left this check disabled by default,
> > keeping in line with the goal of having a low-noise checker. If interested
> > in using it, I have included an example below of how to enable this check.
> >
> > Current State of Alarms:
> > With x86-64 on Linux v7.2-rc4 (which this RFC is based), there are 1282
> > alarms coming from the default set of checks, and an additional 976 alarms
> > if enabling the optional select-visible check. The last time I checked
> > linux-next (next-20260427), there were 81 unique dead links.
> >
> > The most critical check is the dead default statements, which has surfaced
> > a few misconfiguration bugs (fortunately, just for kunit tests), see
> > examples:
> > https://lore.kernel.org/all/20260323124118.1414913-1-julianbraha@gmail.com/
> > and:
> > https://lore.kernel.org/all/20260323123536.1413732-1-julianbraha@gmail.com/
> >
> > But hopefully kconfirm can ease maintenance and we can prevent more of
> > these from making it into the tree in the future.
> >
> > Use it:
> > You can test out kconfirm with this patch series by compiling and running
> > kconfirm like this:
> >
> > `make kconfirm`
> >
> > To enable the select-visible check:
> > `KCONFIRM_ARGS="--enable-check select_visible" make kconfirm`
> >
> > And to enable dead link checks in the help texts:
> > `KCONFIRM_ARGS="--enable-check dead_link" make kconfirm`
> >
> > kconfirm by default runs on the same architecture as the kernel build
> > would. To run kconfirm on another architecture (for example, ARM with an
> > X86 host):
> > `ARCH=arm make kconfirm`
> >
> > Thanks,
> > Julian Braha
> > ---
> > Changes since RFC v3:
> > - Modify kconfig parser to make raw parse tree viewable to enable analysis
> > - Switch from external parser to in-tree kconfig parser (Demi)
> > - Add Rust bindings for kconfig
> > - Remove Cargo and external crates
> > - Make curl an optional dependency for optional dead link check (Arnd)
> > - Switch from libcurl to curl CLI for dead link checks (Miguel)
> > - Adhere to Rust-for-Linux style (Miguel)
> > - Add tests (Miguel)
> > - Move kconfirm under scripts/kconfig/ to resolve broken tab autocompletion (Nathan)
> > - Remove ungrouped attributes style check
> > - Add support for checking liveness of ftp and git URIs in help texts
> > - Dropped RFC tag
> >
> > Link to RFC v3:
> > https://lore.kernel.org/all/20260516215354.449807-1-julianbraha@gmail.com/
> >
> > Changes since RFC v2:
> > - Reduce Rust dependencies significantly (follows Demi's suggestions):
> > - from 6 direct dependencies to 1
> > - from 107 indirect dependencies to 4
> > - Replace ureq crate with usage of system libcurl (thanks Demi)
> > - Replace clap crate with FFI bindings to libc's getopt_long (also Demi)
> > - Remove crates env_logger, regex
> > - Switch from vendoring dependencies to requiring users to first download
> > outside of Make (as suggested by Miguel)
> > - Various makefile improvements (as pointed out by Nicolas):
> > - Fix out-of-tree builds
> > - Only delete kconfirm artifacts with 'distclean' and 'mrproper'
> > - Add myself as maintainer of kconfirm (as discussed with Nicolas)
> > - Remove dedicated code license file (pointed out by Jani)
> > - Update documentation to explain tool setup
> > - Add hint to users to check documentation and download tool dependencies
> > - Address sashiko's many code-level and documentation suggestions:
> > - Follow the kernel's rust import style
> > - Fix a dead_range/duplicate_range alarm mixup
> > - Fix potential duplicates in default value style check
> > - Avoid panicking on errors
> > - Clarify parse failure check usage in documentation
> > - Fix typo in documentation
> > - Can now enable architectures and disable the default (host) architecture in the CLI
> >
> > Link to RFC v2:
> > https://lore.kernel.org/all/20260509203808.1142311-1-julianbraha@gmail.com/
> >
> > Changes since RFC v1:
> > - vendored dependencies instead of requiring an internet connection
> > - removed Cargo.lock
> > - replaced reqwest dependency with smaller ureq
> > - removed rustls, expect user to have openssl instead
> > - added select-visible check based on Jani's feature request
> > - added invalid (reverse) range check
> > - deduplicating alarms that appear for multiple architectures
> > - `make clean` no longer deletes kconfirm's build artifacts
> > - typo fixes in documentation
> > - added patch description for the main "add kconfirm" patch (patch 1/2)
> >
> > Link to RFC v1:
> > https://lore.kernel.org/all/20260427174429.779474-1-julianbraha@gmail.com/
> > ---
> >
> > Julian Braha (5):
> > kconfig: add add another callback to the parser to view raw parse tree
> > kconfig: add kconfirm
> > kconfirm: add tests
> > Documentation: add kconfirm
> > MAINTAINERS: add entry for kconfirm
> >
> > Documentation/dev-tools/index.rst | 1 +
> > Documentation/dev-tools/kconfirm.rst | 229 ++++++
> > MAINTAINERS | 7 +
> > Makefile | 17 +-
> > scripts/kconfig/.gitignore | 1 +
> > scripts/kconfig/Makefile | 54 ++
> > scripts/kconfig/kconfig.rs | 445 +++++++++++
> > scripts/kconfig/kconfirm/.gitignore | 2 +
> > scripts/kconfig/kconfirm/analyze.rs | 340 ++++++++
> > scripts/kconfig/kconfirm/arch.rs | 53 ++
> > scripts/kconfig/kconfirm/checks.rs | 748 ++++++++++++++++++
> > scripts/kconfig/kconfirm/dead_links.rs | 230 ++++++
> > scripts/kconfig/kconfirm/kconfirm-cfg.sh | 57 ++
> > scripts/kconfig/kconfirm/kconfirm.rs | 278 +++++++
> > scripts/kconfig/kconfirm/output.rs | 87 ++
> > scripts/kconfig/kconfirm/symbol_table.rs | 105 +++
> > .../kconfig/kconfirm/tests/arch/arm/Kconfig | 9 +
> > .../kconfirm/tests/arch/powerpc/Kconfig | 4 +
> > .../kconfig/kconfirm/tests/arch/riscv/Kconfig | 9 +
> > .../kconfig/kconfirm/tests/arch/sh/Kconfig | 4 +
> > .../kconfirm/tests/arch/testarch/Kconfig | 4 +
> > .../kconfig/kconfirm/tests/arch/um/Kconfig | 4 +
> > .../kconfig/kconfirm/tests/arch/x86/Kconfig | 4 +
> > .../kconfirm/tests/architecture.Kconfig | 4 +
> > .../tests/architecture_common.Kconfig | 19 +
> > .../kconfirm/tests/conditional_prompt.Kconfig | 17 +
> > scripts/kconfig/kconfirm/tests/conftest.py | 93 +++
> > ...nt_condition_negative_expression_1.Kconfig | 13 +
> > ...nt_condition_negative_expression_2.Kconfig | 13 +
> > ...nstant_condition_negative_symbol_1.Kconfig | 13 +
> > ...nstant_condition_negative_symbol_2.Kconfig | 13 +
> > .../kconfig/kconfirm/tests/dead_link.Kconfig | 12 +
> > .../tests/default_categorization.Kconfig | 20 +
> > scripts/kconfig/kconfirm/tests/pytest.ini | 2 +
> > scripts/kconfig/kconfirm/tests/ranges.Kconfig | 39 +
> > .../kconfirm/tests/select_imply.Kconfig | 28 +
> > .../kconfig/kconfirm/tests/test_kconfirm.py | 358 +++++++++
> > scripts/kconfig/lkc_proto.h | 2 +
> > scripts/kconfig/parser.y | 21 +
> > 39 files changed, 3357 insertions(+), 2 deletions(-)
> > create mode 100644 Documentation/dev-tools/kconfirm.rst
> > create mode 100644 scripts/kconfig/kconfig.rs
> > create mode 100644 scripts/kconfig/kconfirm/.gitignore
> > create mode 100644 scripts/kconfig/kconfirm/analyze.rs
> > create mode 100644 scripts/kconfig/kconfirm/arch.rs
> > create mode 100644 scripts/kconfig/kconfirm/checks.rs
> > create mode 100644 scripts/kconfig/kconfirm/dead_links.rs
> > create mode 100755 scripts/kconfig/kconfirm/kconfirm-cfg.sh
> > create mode 100644 scripts/kconfig/kconfirm/kconfirm.rs
> > create mode 100644 scripts/kconfig/kconfirm/output.rs
> > create mode 100644 scripts/kconfig/kconfirm/symbol_table.rs
> > create mode 100644 scripts/kconfig/kconfirm/tests/arch/arm/Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/arch/powerpc/Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/arch/riscv/Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/arch/sh/Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/arch/testarch/Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/arch/um/Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/arch/x86/Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/architecture.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/architecture_common.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/conditional_prompt.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/conftest.py
> > create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_1.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_expression_2.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_1.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/constant_condition/default_constant_condition_negative_symbol_2.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/dead_link.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/default_categorization.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/pytest.ini
> > create mode 100644 scripts/kconfig/kconfirm/tests/ranges.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/select_imply.Kconfig
> > create mode 100644 scripts/kconfig/kconfirm/tests/test_kconfirm.py
> >
>
> As I've been getting more comfortable with the in-tree Kconfig
> interpreter, I've been wondering if the community might prefer the
> gradual integration of lints directly into Kconfig, instead of as a
> standalone tool. Similar to a comment by Jani on another patch [1].
Yes, I'd prefer if in-tree Kconfig is improved to find more mistakes in
Kconfig files itself.
Thanks for keeping up!
Kind regards,
Nicolas
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v4 0/5] add kconfirm
2026-09-04 12:29 ` [PATCH v4 0/5] add kconfirm Julian Braha
2026-09-04 18:37 ` Nicolas Schier
@ 2026-09-04 22:05 ` Nathan Chancellor
2026-09-06 15:22 ` Julian Braha
1 sibling, 1 reply; 10+ messages in thread
From: Nathan Chancellor @ 2026-09-04 22:05 UTC (permalink / raw)
To: Julian Braha
Cc: nsc, ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs,
andrew.jones, masahiroy, corbet, qingfang.deng, demiobenour, ej,
linux-kernel, rust-for-linux, linux-doc, linux-kbuild
On Fri, Sep 04, 2026 at 01:29:16PM +0100, Julian Braha wrote:
> As I've been getting more comfortable with the in-tree Kconfig
> interpreter, I've been wondering if the community might prefer the
> gradual integration of lints directly into Kconfig, instead of as a
> standalone tool. Similar to a comment by Jani on another patch [1].
Yes, I would think from a user/general developer perspective, it would
be more useful (and frankly, effective) to have these sort of
lints/checks in Kconfig directly so that they just run as part of a
normal build.
> Otherwise, I can do a round of self-review and send a v5 for this
> series. I already noticed some dumb typos in the subject lines, but any
> other feedback on v4 would be welcome if kconfirm is the preferred
> route :)
My apologies for not getting to this sooner, I have been busy with some
other projects. I will try to play around with this from a usability
perspective (which is what I am most concerned with at this point). If a
v5 is mostly prepared, I would not mind seeing it even if we want to go
the in-tree interpreter checks. One option is to take kconfirm into the
tree, fix up the warnings it generates, then moving the checks into the
interpreter directly while dropping them from kconfirm (since the tree
should be clean at that point). I am open to opinions on that though.
--
Cheers,
Nathan
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v4 0/5] add kconfirm
2026-09-04 22:05 ` Nathan Chancellor
@ 2026-09-06 15:22 ` Julian Braha
0 siblings, 0 replies; 10+ messages in thread
From: Julian Braha @ 2026-09-06 15:22 UTC (permalink / raw)
To: Nathan Chancellor
Cc: nsc, ojeda, akpm, jani.nikula, gary, gregkh, arnd, ljs,
andrew.jones, masahiroy, corbet, qingfang.deng, demiobenour, ej,
linux-kernel, rust-for-linux, linux-doc, linux-kbuild
On 9/4/26 23:05, Nathan Chancellor wrote:
> My apologies for not getting to this sooner, I have been busy with some
> other projects. I will try to play around with this from a usability
> perspective (which is what I am most concerned with at this point). If a
> v5 is mostly prepared, I would not mind seeing it even if we want to go
> the in-tree interpreter checks. One option is to take kconfirm into the
> tree, fix up the warnings it generates, then moving the checks into the
> interpreter directly while dropping them from kconfirm (since the tree
> should be clean at that point). I am open to opinions on that though.
>
Your strategy makes sense to me. I'll send v5 then, but I'd also like to
make sure that Nicolas is onboard with this idea.
- Julian Braha
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-09-06 15:22 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-27 0:16 [PATCH v4 0/5] add kconfirm Julian Braha
2026-07-27 0:16 ` [PATCH v4 1/5] kconfig: add add another callback to the parser to view raw parse tree Julian Braha
2026-07-27 0:16 ` [PATCH 2/5] kconfig: add kconfirm Julian Braha
2026-07-27 0:16 ` [PATCH v4 3/5] kconfirm: add tests Julian Braha
2026-07-27 0:16 ` [PATCH v4 4/5] Documentation: add kconfirm Julian Braha
2026-07-27 0:16 ` [PATCH v4 5/5] MAINTAINERS: add entry for kconfirm Julian Braha
2026-09-04 12:29 ` [PATCH v4 0/5] add kconfirm Julian Braha
2026-09-04 18:37 ` Nicolas Schier
2026-09-04 22:05 ` Nathan Chancellor
2026-09-06 15:22 ` Julian Braha
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox