vsec 0.0.1

Detect secrets and in Rust codebases
Documentation
// src/parser/context.rs

use std::collections::HashMap;

/// Parse-time context for tracking scope and symbols
#[derive(Debug, Clone, Default)]
pub struct ParseContext {
    /// Stack of scopes (module names, function names, etc.)
    scope_stack: Vec<ScopeEntry>,

    /// Local symbol table for this file
    symbols: HashMap<String, SymbolInfo>,

    /// Whether we're currently in a test context
    pub in_test: bool,

    /// Whether we're in a macro expansion
    pub in_macro: bool,

    /// Current nesting depth
    depth: usize,
}

/// An entry in the scope stack
#[derive(Debug, Clone)]
pub enum ScopeEntry {
    Module(String),
    Function(String),
    Impl { type_name: String },
    Block,
    Test,
}

/// Information about a symbol
#[derive(Debug, Clone)]
pub struct SymbolInfo {
    /// The name of the symbol
    pub name: String,

    /// The value (if it's a constant with a known value)
    pub value: Option<String>,

    /// The kind of symbol
    pub kind: SymbolKind,

    /// Line number where defined
    pub line: u32,
}

/// Kind of symbol
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolKind {
    Constant,
    Static,
    Function,
    Variable,
    Local,
    Type,
}

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

    /// Push a new scope entry
    pub fn push_scope(&mut self, entry: ScopeEntry) {
        if matches!(entry, ScopeEntry::Test) {
            self.in_test = true;
        }
        self.scope_stack.push(entry);
        self.depth += 1;
    }

    /// Pop the current scope
    pub fn pop_scope(&mut self) {
        if let Some(entry) = self.scope_stack.pop() {
            if matches!(entry, ScopeEntry::Test) {
                // Only reset if no other test scopes
                self.in_test = self.scope_stack.iter().any(|e| matches!(e, ScopeEntry::Test));
            }
            self.depth = self.depth.saturating_sub(1);
        }
    }

    /// Get the current scope path (e.g., "module::submodule::function")
    pub fn scope_path(&self) -> String {
        self.scope_stack
            .iter()
            .filter_map(|e| match e {
                ScopeEntry::Module(name) => Some(name.as_str()),
                ScopeEntry::Function(name) => Some(name.as_str()),
                ScopeEntry::Impl { type_name } => Some(type_name.as_str()),
                ScopeEntry::Block | ScopeEntry::Test => None,
            })
            .collect::<Vec<_>>()
            .join("::")
    }

    /// Register a symbol in the local table
    pub fn register_symbol(&mut self, name: String, info: SymbolInfo) {
        self.symbols.insert(name, info);
    }

    /// Look up a symbol by name
    pub fn lookup_symbol(&self, name: &str) -> Option<&SymbolInfo> {
        self.symbols.get(name)
    }

    /// Get the value of a constant or local symbol
    pub fn get_constant_value(&self, name: &str) -> Option<&str> {
        self.symbols.get(name).and_then(|s| {
            if s.kind == SymbolKind::Constant
                || s.kind == SymbolKind::Static
                || s.kind == SymbolKind::Local
            {
                s.value.as_deref()
            } else {
                None
            }
        })
    }

    /// Check if currently in a test context
    pub fn is_test_context(&self) -> bool {
        self.in_test
    }

    /// Get current nesting depth
    pub fn depth(&self) -> usize {
        self.depth
    }

    /// Check if in a module with the given name
    pub fn in_module(&self, name: &str) -> bool {
        self.scope_stack.iter().any(|e| {
            if let ScopeEntry::Module(m) = e {
                m == name
            } else {
                false
            }
        })
    }

    /// Check if in a function with the given name
    pub fn in_function(&self, name: &str) -> bool {
        self.scope_stack.iter().any(|e| {
            if let ScopeEntry::Function(f) = e {
                f == name
            } else {
                false
            }
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_scope_path() {
        let mut ctx = ParseContext::new();
        ctx.push_scope(ScopeEntry::Module("foo".into()));
        ctx.push_scope(ScopeEntry::Module("bar".into()));
        ctx.push_scope(ScopeEntry::Function("baz".into()));

        assert_eq!(ctx.scope_path(), "foo::bar::baz");
    }

    #[test]
    fn test_test_context() {
        let mut ctx = ParseContext::new();
        assert!(!ctx.is_test_context());

        ctx.push_scope(ScopeEntry::Test);
        assert!(ctx.is_test_context());

        ctx.pop_scope();
        assert!(!ctx.is_test_context());
    }

    #[test]
    fn test_symbol_lookup() {
        let mut ctx = ParseContext::new();
        ctx.register_symbol(
            "API_KEY".into(),
            SymbolInfo {
                name: "API_KEY".into(),
                value: Some("secret".into()),
                kind: SymbolKind::Constant,
                line: 10,
            },
        );

        assert_eq!(ctx.get_constant_value("API_KEY"), Some("secret"));
        assert_eq!(ctx.get_constant_value("UNKNOWN"), None);
    }
}