kconfig-lsp 0.1.0

A language server for Kconfig files used in Linux, Zephyr, U-Boot, and coreboot
Documentation
#![allow(dead_code)]

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::ast::*;
use crate::lexer::Lexer;
use crate::parser;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefKind {
    Config,
    MenuConfig,
    Choice,
}

#[derive(Debug, Clone)]
pub struct SymbolDef {
    pub name: String,
    pub kind: DefKind,
    pub name_span: Span,
    pub type_kind: Option<TypeKind>,
    pub prompt: Option<String>,
    pub help: Option<String>,
    pub file: PathBuf,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefKind {
    DependsOn,
    Select,
    Imply,
    Default,
    Range,
    VisibleIf,
    IfCondition,
}

#[derive(Debug, Clone)]
pub struct SymbolRef {
    pub name: String,
    pub kind: RefKind,
    pub span: Span,
    pub file: PathBuf,
}

#[derive(Debug, Clone)]
pub struct FileAnalysis {
    pub file: KconfigFile,
    pub line_index: LineIndex,
    pub source: String,
    pub diagnostics: Vec<ParseDiagnostic>,
}

#[derive(Debug, Default)]
pub struct WorldIndex {
    pub definitions: HashMap<String, Vec<SymbolDef>>,
    pub references: HashMap<String, Vec<SymbolRef>>,
    pub all_symbols: Vec<String>,
    pub files: HashMap<PathBuf, FileAnalysis>,
}

impl WorldIndex {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn analyze_file(&mut self, path: &Path, source: &str) {
        let tokens = Lexer::new(source).tokenize();
        let result = parser::parse(source, tokens);
        let line_index = LineIndex::new(source);

        let file_path = path.to_path_buf();
        let mut defs = Vec::new();
        let mut refs = Vec::new();

        collect_entries(&result.file.entries, &file_path, &mut defs, &mut refs);

        for d in &defs {
            self.definitions
                .entry(d.name.clone())
                .or_default()
                .push(d.clone());
            if !self.all_symbols.contains(&d.name) {
                self.all_symbols.push(d.name.clone());
            }
        }
        for r in &refs {
            self.references
                .entry(r.name.clone())
                .or_default()
                .push(r.clone());
        }

        self.files.insert(
            file_path,
            FileAnalysis {
                file: result.file,
                line_index,
                source: source.to_string(),
                diagnostics: result.diagnostics,
            },
        );
    }

    pub fn remove_file(&mut self, path: &Path) {
        self.files.remove(path);

        self.definitions.retain(|_, defs| {
            defs.retain(|d| d.file != path);
            !defs.is_empty()
        });
        self.references.retain(|_, refs| {
            refs.retain(|r| r.file != path);
            !refs.is_empty()
        });
        self.all_symbols = self.definitions.keys().cloned().collect();
    }

    pub fn reanalyze_file(&mut self, path: &Path, source: &str) {
        self.remove_file(path);
        self.analyze_file(path, source);
    }

    pub fn get_definitions(&self, name: &str) -> &[SymbolDef] {
        self.definitions
            .get(name)
            .map(|v| v.as_slice())
            .unwrap_or(&[])
    }

    pub fn get_references(&self, name: &str) -> &[SymbolRef] {
        self.references
            .get(name)
            .map(|v| v.as_slice())
            .unwrap_or(&[])
    }
}

fn collect_entries(
    entries: &[Entry],
    file: &Path,
    defs: &mut Vec<SymbolDef>,
    refs: &mut Vec<SymbolRef>,
) {
    for entry in entries {
        match entry {
            Entry::Config(c) | Entry::MenuConfig(c) => {
                let kind = if matches!(entry, Entry::MenuConfig(_)) {
                    DefKind::MenuConfig
                } else {
                    DefKind::Config
                };
                let mut type_kind = None;
                let mut prompt = None;
                let mut help = None;

                for attr in &c.attributes {
                    match attr {
                        Attribute::Type(t) => {
                            type_kind = Some(t.kind);
                            if let Some(p) = &t.prompt {
                                prompt = Some(p.text.clone());
                            }
                        }
                        Attribute::DefType(dt) => {
                            type_kind = Some(dt.kind);
                        }
                        Attribute::Prompt(p) => {
                            prompt = Some(p.text.clone());
                        }
                        Attribute::Help(h) => {
                            help = Some(h.text.clone());
                        }
                        _ => {}
                    }
                    collect_attr_refs(attr, file, refs);
                }

                defs.push(SymbolDef {
                    name: c.name.clone(),
                    kind,
                    name_span: c.name_span,
                    type_kind,
                    prompt,
                    help,
                    file: file.to_path_buf(),
                });
            }
            Entry::Choice(ch) => {
                for attr in &ch.attributes {
                    collect_attr_refs(attr, file, refs);
                }
                collect_entries(&ch.entries, file, defs, refs);
            }
            Entry::Comment(cm) => {
                for attr in &cm.attributes {
                    collect_attr_refs(attr, file, refs);
                }
            }
            Entry::Menu(m) => {
                for attr in &m.attributes {
                    collect_attr_refs(attr, file, refs);
                }
                collect_entries(&m.entries, file, defs, refs);
            }
            Entry::If(i) => {
                collect_expr_refs(&i.condition, RefKind::IfCondition, file, refs);
                collect_entries(&i.entries, file, defs, refs);
            }
            Entry::Source(_) | Entry::MainMenu(_) => {}
        }
    }
}

fn collect_attr_refs(attr: &Attribute, file: &Path, refs: &mut Vec<SymbolRef>) {
    match attr {
        Attribute::DependsOn(d) => {
            collect_expr_refs(&d.expr, RefKind::DependsOn, file, refs);
        }
        Attribute::Select(s) => {
            refs.push(SymbolRef {
                name: s.symbol.clone(),
                kind: RefKind::Select,
                span: s.symbol_span,
                file: file.to_path_buf(),
            });
            if let Some(cond) = &s.condition {
                collect_expr_refs(cond, RefKind::Select, file, refs);
            }
        }
        Attribute::Imply(i) => {
            refs.push(SymbolRef {
                name: i.symbol.clone(),
                kind: RefKind::Imply,
                span: i.symbol_span,
                file: file.to_path_buf(),
            });
            if let Some(cond) = &i.condition {
                collect_expr_refs(cond, RefKind::Imply, file, refs);
            }
        }
        Attribute::Default(d) => {
            collect_expr_refs(&d.value, RefKind::Default, file, refs);
            if let Some(cond) = &d.condition {
                collect_expr_refs(cond, RefKind::Default, file, refs);
            }
        }
        Attribute::DefType(dt) => {
            collect_expr_refs(&dt.value, RefKind::Default, file, refs);
            if let Some(cond) = &dt.condition {
                collect_expr_refs(cond, RefKind::Default, file, refs);
            }
        }
        Attribute::VisibleIf(v) => {
            collect_expr_refs(&v.expr, RefKind::VisibleIf, file, refs);
        }
        Attribute::Range(r) => {
            collect_expr_refs(&r.low, RefKind::Range, file, refs);
            collect_expr_refs(&r.high, RefKind::Range, file, refs);
            if let Some(cond) = &r.condition {
                collect_expr_refs(cond, RefKind::Range, file, refs);
            }
        }
        Attribute::Type(t) => {
            if let Some(p) = &t.prompt {
                if let Some(cond) = &p.condition {
                    collect_expr_refs(cond, RefKind::DependsOn, file, refs);
                }
            }
        }
        Attribute::Prompt(p) => {
            if let Some(cond) = &p.condition {
                collect_expr_refs(cond, RefKind::DependsOn, file, refs);
            }
        }
        Attribute::Help(_)
        | Attribute::Modules(_)
        | Attribute::Transitional(_)
        | Attribute::Optional(_) => {}
    }
}

fn collect_expr_refs(expr: &Expr, kind: RefKind, file: &Path, refs: &mut Vec<SymbolRef>) {
    let mut syms = Vec::new();
    expr.collect_symbols(&mut syms);
    for (name, span) in syms {
        if is_tristate_literal(&name) || name.is_empty() || is_numeric_literal(&name) {
            continue;
        }
        refs.push(SymbolRef {
            name,
            kind,
            span,
            file: file.to_path_buf(),
        });
    }
}

fn is_tristate_literal(s: &str) -> bool {
    matches!(s, "y" | "n" | "m")
}

fn is_numeric_literal(s: &str) -> bool {
    if s.starts_with("0x") || s.starts_with("0X") {
        s.len() > 2 && s[2..].chars().all(|c| c.is_ascii_hexdigit())
    } else {
        !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
    }
}