crates/nornir-purgatory/src/kconfig/tree.rs
//! **Kconfig as the dependency graph it already is** — the [`crate::KconfigSource`]
//! over a real kernel tree.
//!
//! `mimir-v2.md` §2: *"what does `CONFIG_X` pull in? — Kconfig itself.
//! `select` / `depends on` / `default` IS a declarative dependency graph, in the
//! tree, machine-readable. No C scan."* This module reads it: from the root
//! `Kconfig`, following every `source` (with `$(SRCARCH)` substituted), through
//! `if`/`menu`/`choice` scopes whose conditions become dependencies of what they
//! enclose, past help blocks (prose, never attributes), into a symbol table with
//! a reverse `select` index.
//!
//! Three questions are answered from it and nothing else:
//!
//! * **what does enabling `X` force on?** — [`KconfigTree::select_closure`]
//! * **what does policy imply?** — [`KconfigTree::policy_closure`]: a fragment
//! that says `SECCOMP_FILTER=y` has also said `SECCOMP` and `NET`, because
//! `olddefconfig` would drop the first without the others. Invariant 2 must
//! therefore protect the closure, not just the named set.
//! * **why is this here?** — [`KconfigTree::explain_arrival`]: the enabled
//! symbol that `select`ed an `ADDED` one, with the file it is written in. The
//! 2026-09-07 `BLK_DEV_IO_TRACE select DEBUG_FS` finding, made routine.
//!
//! It is a reader, not a solver. It does not evaluate expressions, does not know
//! a symbol's value, and never predicts an effective set — that is the produced
//! `.config`'s job and [`super::effective`]'s honesty rule.
//!
//! Zero dependencies: a hand-rolled tokenizer over `std::fs`. A kernel tree is
//! ~1 900 Kconfig files and reads in well under a second.
use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
use std::fmt;
use std::path::{Path, PathBuf};
/// A symbol's type, from `bool`/`tristate`/`def_bool`/…
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Bool,
Tristate,
Int,
Hex,
Str,
/// Declared without a type line (a bare `config X` seen only via `select`).
Unknown,
}
/// `select TARGET [if COND]` (also `imply`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Select {
pub target: String,
pub cond: Option<String>,
}
/// `default VALUE [if COND]`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DefaultValue {
pub value: String,
pub cond: Option<String>,
}
/// One `config`/`menuconfig` entry, with every block that named it merged.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KSymbol {
pub name: String,
pub kind: Kind,
/// The prompt text, if the symbol is ever prompted. `None` means the user
/// can never toggle it — only `select`/`default` can.
pub prompt: Option<String>,
/// `bool "…" if EXPERT` — the shape `hardened.config:28-36` explains: not
/// prompted on a non-EXPERT defconfig, so `olddefconfig` restores the default.
pub prompt_cond: Option<String>,
/// The symbol's own `depends on` lines, verbatim expressions.
pub depends: Vec<String>,
/// Conditions of the enclosing `if`/`menu`/`choice` scopes — dependencies
/// Kconfig applies without the symbol saying so.
pub scope_depends: Vec<String>,
pub selects: Vec<Select>,
pub implies: Vec<Select>,
pub defaults: Vec<DefaultValue>,
/// Tree-relative file and line of the FIRST block that declared it.
pub file: String,
pub line: usize,
}
impl KSymbol {
fn new(name: &str, file: &str, line: usize) -> KSymbol {
KSymbol {
name: name.to_string(),
kind: Kind::Unknown,
prompt: None,
prompt_cond: None,
depends: Vec::new(),
scope_depends: Vec::new(),
selects: Vec::new(),
implies: Vec::new(),
defaults: Vec::new(),
file: file.to_string(),
line,
}
}
}
/// Who `select`s a symbol, and where that is written.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Selector {
pub by: String,
pub cond: Option<String>,
pub file: String,
pub line: usize,
}
/// How a symbol is mentioned in an expression.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Polarity {
/// `X`, `X=y`, `X=m`, `X!=n`, `X >= 3` — the dependant needs `X` on.
Positive,
/// `!X`, `X=n`, `X!=y` — the dependant needs `X` off; subtracting `X` cannot hurt it.
Negative,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeError {
pub why: String,
}
impl fmt::Display for TreeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.why)
}
}
impl std::error::Error for TreeError {}
/// The graph.
#[derive(Debug, Clone, Default)]
pub struct KconfigTree {
root: Option<PathBuf>,
release: Option<String>,
symbols: BTreeMap<String, KSymbol>,
selected_by: BTreeMap<String, Vec<Selector>>,
files_read: usize,
missing_sources: Vec<String>,
}
impl KconfigTree {
/// Read a kernel tree from disk: `<root>/Kconfig` and everything it sources,
/// with `$(SRCARCH)` = `arch`. A missing root is an error; a missing SOURCED
/// file is recorded in [`KconfigTree::missing_sources`] and reading goes on.
pub fn read(root: &Path, arch: &str) -> Result<KconfigTree, TreeError> {
if !root.join("Kconfig").is_file() {
return Err(TreeError {
why: format!("{} has no Kconfig — not a kernel tree", root.display()),
});
}
let mut tree = KconfigTree {
root: Some(root.to_path_buf()),
..KconfigTree::default()
};
tree.release = read_release(&root.join("Makefile"));
let reader = |rel: &str| std::fs::read_to_string(root.join(rel)).ok();
Parser::new(&mut tree, arch, &reader).parse("Kconfig");
Ok(tree)
}
/// Build from in-memory `(tree-relative path, text)` pairs — for fixtures.
/// The first pair is the root. A `source` naming a path not in the list is
/// recorded as missing, exactly as on disk.
#[must_use]
pub fn from_texts<'a, I>(arch: &str, files: I) -> KconfigTree
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
let map: BTreeMap<String, String> = files
.into_iter()
.map(|(p, t)| (p.to_string(), t.to_string()))
.collect();
let mut tree = KconfigTree::default();
let Some(first) = map.keys().next().cloned() else {
return tree;
};
let reader = |rel: &str| map.get(rel).cloned();
Parser::new(&mut tree, arch, &reader).parse(&first);
tree
}
#[must_use]
pub fn root(&self) -> Option<&Path> {
self.root.as_deref()
}
/// `VERSION.PATCHLEVEL.SUBLEVEL` from the root Makefile, e.g. `6.12.104`.
#[must_use]
pub fn release(&self) -> Option<&str> {
self.release.as_deref()
}
#[must_use]
pub fn len(&self) -> usize {
self.symbols.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.symbols.is_empty()
}
#[must_use]
pub fn files_read(&self) -> usize {
self.files_read
}
/// `source` directives that resolved to nothing — a finding, never silence.
#[must_use]
pub fn missing_sources(&self) -> &[String] {
&self.missing_sources
}
#[must_use]
pub fn symbol(&self, name: &str) -> Option<&KSymbol> {
self.symbols.get(name)
}
pub fn symbols(&self) -> impl Iterator<Item = &KSymbol> {
self.symbols.values()
}
/// Every `select` that names `name`, wherever it is written.
#[must_use]
pub fn selected_by(&self, name: &str) -> &[Selector] {
self.selected_by.get(name).map_or(&[], Vec::as_slice)
}
/// Positive symbols in a symbol's own and scope `depends on`, in order, deduplicated.
#[must_use]
pub fn positive_depends(&self, name: &str) -> Vec<String> {
let Some(s) = self.symbols.get(name) else {
return Vec::new();
};
let mut out = Vec::new();
for expr in s.depends.iter().chain(s.scope_depends.iter()) {
for (sym, pol) in symbols_in_expr(expr) {
if pol == Polarity::Positive && !out.contains(&sym) {
out.push(sym);
}
}
}
out
}
/// **What enabling `name` forces on**, transitively, through `select` alone.
/// `depends on` is not included: a dependency is required, not pulled.
#[must_use]
pub fn select_closure(&self, name: &str) -> BTreeSet<String> {
let mut seen = BTreeSet::new();
let mut queue: VecDeque<String> = VecDeque::from([name.to_string()]);
while let Some(cur) = queue.pop_front() {
if let Some(s) = self.symbols.get(&cur) {
for sel in &s.selects {
if seen.insert(sel.target.clone()) {
queue.push_back(sel.target.clone());
}
}
}
}
seen.remove(name);
seen
}
/// **What `name` needs on**, transitively, through positive `depends on`.
#[must_use]
pub fn depends_closure(&self, name: &str) -> BTreeSet<String> {
let mut seen = BTreeSet::new();
let mut queue: VecDeque<String> = VecDeque::from([name.to_string()]);
while let Some(cur) = queue.pop_front() {
for d in self.positive_depends(&cur) {
if seen.insert(d.clone()) {
queue.push_back(d);
}
}
}
seen.remove(name);
seen
}
/// **What policy implies is policy.** The seeds plus everything they
/// `select` and everything they positively `depend on`, transitively. This
/// is the set invariant 2 must protect: subtract a dependency and
/// `olddefconfig` silently drops the hardening symbol that needed it.
#[must_use]
pub fn policy_closure(&self, seeds: &BTreeSet<String>) -> BTreeSet<String> {
let mut seen: BTreeSet<String> = seeds.clone();
let mut queue: VecDeque<String> = seeds.iter().cloned().collect();
while let Some(cur) = queue.pop_front() {
let mut next: Vec<String> = self.positive_depends(&cur);
if let Some(s) = self.symbols.get(&cur) {
next.extend(s.selects.iter().map(|sel| sel.target.clone()));
}
for n in next {
if seen.insert(n.clone()) {
queue.push_back(n);
}
}
}
seen
}
/// **Why is `added` in the kernel?** The enabled symbols that `select` it.
/// One hop, on purpose: the answer names a symbol somebody can look at, and
/// that symbol's own arrival is the next question.
#[must_use]
pub fn explain_arrival(&self, added: &str, enabled: &BTreeSet<&str>) -> Vec<Selector> {
self.selected_by(added)
.iter()
.filter(|s| enabled.contains(s.by.as_str()))
.cloned()
.collect()
}
}
impl crate::KconfigSource for KconfigTree {
fn selects(&self, symbol: &str) -> Vec<String> {
self.symbols
.get(symbol)
.map(|s| s.selects.iter().map(|sel| sel.target.clone()).collect())
.unwrap_or_default()
}
fn depends(&self, symbol: &str) -> Vec<String> {
self.positive_depends(symbol)
}
}
/// `VERSION = 6` / `PATCHLEVEL = 12` / `SUBLEVEL = 104` from a kernel Makefile.
fn read_release(makefile: &Path) -> Option<String> {
let text = std::fs::read_to_string(makefile).ok()?;
let mut v = [None::<String>, None, None];
for line in text.lines().take(20) {
let Some((k, val)) = line.split_once('=') else {
continue;
};
let idx = match k.trim() {
"VERSION" => 0,
"PATCHLEVEL" => 1,
"SUBLEVEL" => 2,
_ => continue,
};
v[idx] = Some(val.trim().to_string());
}
match v {
[Some(a), Some(b), Some(c)] => Some(format!("{a}.{b}.{c}")),
_ => None,
}
}
// ── the expression tokenizer ─────────────────────────────────────────────────
/// Every symbol an expression mentions, with how it is mentioned.
///
/// Not an evaluator: `depends on` conditions are read for the symbols they
/// NAME, because that is all the closure needs. Constants (`y`/`m`/`n`),
/// numbers, quoted strings and `$(macro …)` calls are not symbols. `!` and a
/// negated group flip polarity; `=n` / `!=y` / `!=m` are negative mentions.
#[must_use]
pub fn symbols_in_expr(expr: &str) -> Vec<(String, Polarity)> {
let chars: Vec<char> = expr.chars().collect();
let mut out = Vec::new();
let mut i = 0;
let mut groups: Vec<bool> = Vec::new(); // negation state of each open paren
let mut pending_not = false;
let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
while i < chars.len() {
let c = chars[i];
let current_neg = groups.last().copied().unwrap_or(false);
if c == '"' {
i += 1;
while i < chars.len() && chars[i] != '"' {
i += 1;
}
i += 1;
pending_not = false;
} else if c == '$' && chars.get(i + 1) == Some(&'(') {
let mut depth = 0;
i += 1;
while i < chars.len() {
match chars[i] {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
i += 1;
break;
}
}
_ => {}
}
i += 1;
}
pending_not = false;
} else if c == '!' && chars.get(i + 1) != Some(&'=') {
pending_not = !pending_not;
i += 1;
} else if c == '(' {
groups.push(current_neg ^ pending_not);
pending_not = false;
i += 1;
} else if c == ')' {
groups.pop();
i += 1;
} else if is_ident(c) {
let start = i;
while i < chars.len() && is_ident(chars[i]) {
i += 1;
}
let word: String = chars[start..i].iter().collect();
let neg = current_neg ^ pending_not;
pending_not = false;
// A comparison operator after the word?
let mut j = i;
while j < chars.len() && chars[j] == ' ' {
j += 1;
}
let op: Option<&str> = if chars[j..].starts_with(&['!', '=']) {
Some("!=")
} else if chars[j..].starts_with(&['>', '=']) || chars[j..].starts_with(&['<', '=']) {
Some("cmp")
} else if chars.get(j) == Some(&'=') {
Some("=")
} else if matches!(chars.get(j), Some('<' | '>')) {
Some("cmp")
} else {
None
};
if let Some(op) = op {
let rhs: String;
j += op.len().min(2);
if op == "cmp" && j < chars.len() && chars[j] == '=' {
j += 1;
}
while j < chars.len() && chars[j] == ' ' {
j += 1;
}
if j < chars.len() && chars[j] == '"' {
let mut k = j + 1;
while k < chars.len() && chars[k] != '"' {
k += 1;
}
rhs = chars[j + 1..k.min(chars.len())].iter().collect();
i = (k + 1).min(chars.len());
} else if j < chars.len() && chars[j] == '$' {
// `X = $(macro)`: skip the macro, no RHS symbol.
i = j;
rhs = String::new();
} else {
let mut k = j;
while k < chars.len() && is_ident(chars[k]) {
k += 1;
}
rhs = chars[j..k].iter().collect();
i = k;
}
let r = rhs.as_str();
if is_symbol_name(&word) {
let pol = match (op, r) {
("=", "n") | ("!=", "y") | ("!=", "m") => Polarity::Negative,
_ => Polarity::Positive,
};
out.push((word.clone(), flip(pol, neg)));
}
if is_symbol_name(r) {
out.push((r.to_string(), flip(Polarity::Positive, neg)));
}
continue;
}
if is_symbol_name(&word) {
out.push((word, flip(Polarity::Positive, neg)));
}
} else {
i += 1;
}
}
out
}
fn flip(p: Polarity, neg: bool) -> Polarity {
match (p, neg) {
(Polarity::Positive, true) => Polarity::Negative,
(Polarity::Negative, true) => Polarity::Positive,
(p, false) => p,
}
}
/// A Kconfig symbol has at least one upper-case ASCII letter and nothing but
/// `[A-Za-z0-9_]` — `y`, `n`, `m`, `if`, `140000` are not symbols.
fn is_symbol_name(s: &str) -> bool {
!s.is_empty()
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& s.chars().any(|c| c.is_ascii_uppercase())
}
// ── the parser ───────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScopeKind {
If,
Menu,
Choice,
}
struct Scope {
kind: ScopeKind,
exprs: Vec<String>,
}
/// Where attribute lines go.
#[derive(Debug, Clone, PartialEq, Eq)]
enum Entity {
None,
Config(String),
/// A `menu` or `choice` header: its `depends on` become the scope's.
TopScope,
Comment,
}
struct Parser<'a> {
tree: &'a mut KconfigTree,
arch: String,
read: &'a dyn Fn(&str) -> Option<String>,
scopes: Vec<Scope>,
visited: HashSet<String>,
}
impl<'a> Parser<'a> {
fn new(
tree: &'a mut KconfigTree,
arch: &str,
read: &'a dyn Fn(&str) -> Option<String>,
) -> Parser<'a> {
Parser {
tree,
arch: arch.to_string(),
read,
scopes: Vec::new(),
visited: HashSet::new(),
}
}
fn parse(&mut self, rel: &str) {
if !self.visited.insert(rel.to_string()) {
return;
}
let Some(text) = (self.read)(rel) else {
self.tree.missing_sources.push(rel.to_string());
return;
};
self.tree.files_read += 1;
self.parse_text(rel, &text);
}
fn scope_exprs(&self) -> Vec<String> {
self.scopes
.iter()
.flat_map(|s| s.exprs.iter().cloned())
.collect()
}
fn pop_until(&mut self, kind: ScopeKind) {
while let Some(s) = self.scopes.pop() {
if s.kind == kind {
break;
}
}
}
fn parse_text(&mut self, file: &str, text: &str) {
let mut entity = Entity::None;
// Help handling: `help_pending` is the indent of the `help` keyword;
// `help_indent` the indent of its first line once seen.
let mut help_pending: Option<usize> = None;
let mut help_indent: Option<usize> = None;
for (idx, raw) in text.lines().enumerate() {
let lineno = idx + 1;
let indent = indent_of(raw);
let blank = raw.trim().is_empty();
if let Some(hi) = help_indent {
if blank || indent >= hi {
continue;
}
help_indent = None;
} else if let Some(kw) = help_pending {
if blank {
continue;
}
help_pending = None;
if indent > kw {
help_indent = Some(indent);
continue;
}
}
let line = strip_comment(raw).trim().to_string();
if line.is_empty() {
continue;
}
let (kw, rest) = match line.split_once(char::is_whitespace) {
Some((k, r)) => (k, r.trim()),
None => (line.as_str(), ""),
};
match kw {
"config" | "menuconfig" => {
let name = rest.split_whitespace().next().unwrap_or("").to_string();
if name.is_empty() {
entity = Entity::None;
continue;
}
let scope = self.scope_exprs();
let sym = self
.tree
.symbols
.entry(name.clone())
.or_insert_with(|| KSymbol::new(&name, file, lineno));
for e in scope {
if !sym.scope_depends.contains(&e) {
sym.scope_depends.push(e);
}
}
entity = Entity::Config(name);
}
"menu" => {
self.scopes.push(Scope {
kind: ScopeKind::Menu,
exprs: Vec::new(),
});
entity = Entity::TopScope;
}
"endmenu" => {
self.pop_until(ScopeKind::Menu);
entity = Entity::None;
}
"choice" => {
self.scopes.push(Scope {
kind: ScopeKind::Choice,
exprs: Vec::new(),
});
entity = Entity::TopScope;
}
"endchoice" => {
self.pop_until(ScopeKind::Choice);
entity = Entity::None;
}
"if" => {
self.scopes.push(Scope {
kind: ScopeKind::If,
exprs: vec![rest.to_string()],
});
entity = Entity::None;
}
"endif" => {
self.pop_until(ScopeKind::If);
entity = Entity::None;
}
"source" => {
let path = unquote(rest);
let path = path
.replace("$(SRCARCH)", &self.arch)
.replace("$(HEADER_ARCH)", &self.arch);
if path.contains("$(") {
self.tree.missing_sources.push(path);
} else {
self.parse(&path);
}
entity = Entity::None;
}
"mainmenu" => entity = Entity::None,
"comment" => entity = Entity::Comment,
"help" | "---help---" => help_pending = Some(indent),
"depends" => {
let expr = rest
.strip_prefix("on")
.map(str::trim)
.unwrap_or(rest)
.to_string();
match &entity {
Entity::Config(n) => {
if let Some(s) = self.tree.symbols.get_mut(n) {
s.depends.push(expr);
}
}
Entity::TopScope => {
if let Some(sc) = self.scopes.last_mut() {
sc.exprs.push(expr);
}
}
_ => {}
}
}
"select" | "imply" => {
if let Entity::Config(n) = &entity {
let (target, cond) = split_if(rest);
let target = target.split_whitespace().next().unwrap_or("").to_string();
if target.is_empty() {
continue;
}
let sel = Select {
target: target.clone(),
cond: cond.clone(),
};
if let Some(s) = self.tree.symbols.get_mut(n) {
if kw == "select" {
s.selects.push(sel);
} else {
s.implies.push(sel);
}
}
if kw == "select" {
self.tree
.selected_by
.entry(target)
.or_default()
.push(Selector {
by: n.clone(),
cond,
file: file.to_string(),
line: lineno,
});
}
}
}
"default" | "def_bool" | "def_tristate" => {
if let Entity::Config(n) = &entity {
let (value, cond) = split_if(rest);
if let Some(s) = self.tree.symbols.get_mut(n) {
s.defaults.push(DefaultValue {
value: value.trim().to_string(),
cond,
});
if kw == "def_bool" {
s.kind = Kind::Bool;
} else if kw == "def_tristate" {
s.kind = Kind::Tristate;
}
}
}
}
"bool" | "tristate" | "string" | "hex" | "int" | "prompt" => {
if let Entity::Config(n) = &entity {
if let Some(s) = self.tree.symbols.get_mut(n) {
match kw {
"bool" => s.kind = Kind::Bool,
"tristate" => s.kind = Kind::Tristate,
"string" => s.kind = Kind::Str,
"hex" => s.kind = Kind::Hex,
"int" => s.kind = Kind::Int,
_ => {}
}
if rest.starts_with('"') {
let (prompt, after) = take_quoted(rest);
s.prompt = Some(prompt);
let after = after.trim();
if let Some(c) = after.strip_prefix("if ") {
s.prompt_cond = Some(c.trim().to_string());
}
}
}
}
}
// `range`, `option`, `modules`, `visible if`, and the macro
// assignments in scripts/Kconfig.include: not graph.
_ => {}
}
}
}
}
fn indent_of(line: &str) -> usize {
let mut n = 0;
for c in line.chars() {
match c {
'\t' => n += 8,
' ' => n += 1,
_ => break,
}
}
n
}
/// Drop a `# comment` that is not inside double quotes.
fn strip_comment(line: &str) -> &str {
let mut in_q = false;
for (i, c) in line.char_indices() {
match c {
'"' => in_q = !in_q,
'#' if !in_q => return &line[..i],
_ => {}
}
}
line
}
fn unquote(s: &str) -> String {
let s = s.trim();
s.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(s)
.to_string()
}
/// `"text" rest` → (`text`, `rest`).
fn take_quoted(s: &str) -> (String, &str) {
let Some(body) = s.strip_prefix('"') else {
return (String::new(), s);
};
match body.find('"') {
Some(end) => (body[..end].to_string(), &body[end + 1..]),
None => (body.to_string(), ""),
}
}
/// Split `VALUE if COND` at the LAST ` if ` that is outside quotes and parens.
fn split_if(s: &str) -> (String, Option<String>) {
let chars: Vec<char> = s.chars().collect();
let mut in_q = false;
let mut depth = 0i32;
let mut cut: Option<usize> = None;
let mut i = 0;
while i < chars.len() {
match chars[i] {
'"' => in_q = !in_q,
'(' if !in_q => depth += 1,
')' if !in_q => depth -= 1,
' ' if !in_q && depth == 0 && chars[i + 1..].starts_with(&['i', 'f', ' ']) => {
cut = Some(i)
}
_ => {}
}
i += 1;
}
match cut {
Some(c) => {
let value: String = chars[..c].iter().collect();
let cond: String = chars[c + 4..].iter().collect();
(value.trim().to_string(), Some(cond.trim().to_string()))
}
None => (s.trim().to_string(), None),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_if_finds_the_top_level_if_only() {
assert_eq!(split_if("DEBUG_FS"), ("DEBUG_FS".into(), None));
assert_eq!(
split_if("HAVE_ARCH_HUGE_VMAP if X86_64 || X86_PAE"),
(
"HAVE_ARCH_HUGE_VMAP".into(),
Some("X86_64 || X86_PAE".into())
)
);
assert_eq!(
split_if("\"a string with if inside\" if EXPERT"),
("\"a string with if inside\"".into(), Some("EXPERT".into()))
);
assert_eq!(
split_if("$(if-success,x,y,n)"),
("$(if-success,x,y,n)".into(), None)
);
}
#[test]
fn comments_outside_quotes_are_stripped() {
assert_eq!(strip_comment("endif # HYPERVISOR_GUEST").trim(), "endif");
assert_eq!(
strip_comment("bool \"a # in a prompt\""),
"bool \"a # in a prompt\""
);
assert_eq!(strip_comment("# whole line"), "");
}
#[test]
fn a_help_block_ends_where_the_indent_falls_back() {
let t = KconfigTree::from_texts(
"x86",
[(
"Kconfig",
"config A\n\tbool \"a\"\n\thelp\n\t prose\n\n\t select B is prose too\n\nconfig B\n\tbool\n\tselect C\n\nconfig C\n\tbool\n",
)],
);
assert!(t.symbol("A").unwrap().selects.is_empty());
assert_eq!(t.symbol("B").unwrap().selects[0].target, "C");
assert_eq!(t.len(), 3);
}
#[test]
fn multiple_blocks_for_one_symbol_merge_and_keep_the_first_site() {
let t = KconfigTree::from_texts(
"x86",
[
(
"Kconfig",
"config A\n\tbool\n\tselect B\nsource \"b/Kconfig\"\n",
),
("b/Kconfig", "config A\n\tselect C\n\tdepends on D\n"),
],
);
let a = t.symbol("A").unwrap();
assert_eq!(a.file, "Kconfig");
assert_eq!(a.selects.len(), 2);
assert_eq!(a.depends, ["D"]);
assert_eq!(t.files_read(), 2);
}
}