ryo-suggest 0.1.0

[experimental] Pattern-based suggestion engine for RYO
Documentation
//! Allow filtering for suggestions based on @spec:allow directives.
//!
//! This module provides functionality to check if a suggestion should be
//! skipped based on `@spec:allow(...)` directives in doc comments.
//!
//! # Example
//!
//! ```rust,ignore
//! /// @spec:allow(RL001, RL002)
//! struct LegacyConfig {
//!     // This struct will not trigger RL001 or RL002 suggestions
//! }
//! ```

use std::collections::HashMap;

use ryo_analysis::context::AnalysisContext;
use ryo_analysis::symbol::WorkspaceFilePath;
use ryo_analysis::SymbolId;
use ryo_source::pure::PureFile;
use ryo_spec::comment::{CommentSpec, CommentSpecExtractor};

/// Store for allow directives extracted from source files.
///
/// Caches CommentSpec information per symbol name for efficient lookups
/// during suggestion filtering.
#[derive(Debug, Default)]
pub struct AllowStore {
    /// Map from symbol name to its CommentSpec (contains allow directives)
    specs: HashMap<String, CommentSpec>,
}

impl AllowStore {
    /// Create a new empty AllowStore.
    pub fn new() -> Self {
        Self::default()
    }

    /// Build AllowStore from AnalysisContext.
    ///
    /// Extracts all CommentSpec directives from all files in the context.
    pub fn from_context(ctx: &AnalysisContext) -> Self {
        let mut store = Self::new();
        let extractor = CommentSpecExtractor::new();

        for (path, file) in ctx.files() {
            store.extract_from_file(path, file, &extractor);
        }

        store
    }

    /// Extract allow directives from a single file.
    fn extract_from_file(
        &mut self,
        _path: &WorkspaceFilePath,
        file: &PureFile,
        extractor: &CommentSpecExtractor,
    ) {
        let specs = extractor.extract(file);
        for spec in specs {
            // Store by target name (e.g., "LegacyConfig")
            self.specs.insert(spec.target.clone(), spec);
        }
    }

    /// Check if a rule is allowed for a specific symbol.
    ///
    /// Returns true if the rule should be skipped (i.e., is allowed).
    pub fn is_allowed(&self, symbol_name: &str, rule_id: &str) -> bool {
        if let Some(spec) = self.specs.get(symbol_name) {
            spec.is_rule_allowed(rule_id)
        } else {
            false
        }
    }

    /// Check if a rule is allowed for any of the given symbol IDs.
    ///
    /// Looks up symbol names from the registry and checks allow directives.
    pub fn is_allowed_for_symbols(
        &self,
        ctx: &AnalysisContext,
        symbol_ids: &[SymbolId],
        rule_id: &str,
    ) -> bool {
        for &symbol_id in symbol_ids {
            if let Some(path) = ctx.registry.path(symbol_id) {
                // Get the symbol name (last component of the path)
                let symbol_name = path.name();
                if self.is_allowed(symbol_name, rule_id) {
                    return true;
                }
            }
        }
        false
    }

    /// Get the CommentSpec for a symbol if it exists.
    pub fn get(&self, symbol_name: &str) -> Option<&CommentSpec> {
        self.specs.get(symbol_name)
    }

    /// Get the number of specs in the store.
    pub fn len(&self) -> usize {
        self.specs.len()
    }

    /// Check if the store is empty.
    pub fn is_empty(&self) -> bool {
        self.specs.is_empty()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ryo_source::ItemKind;
    use ryo_spec::comment::SpecDirective;

    #[test]
    fn test_allow_store_basic() {
        let mut store = AllowStore::new();

        // Manually add a spec with allow directive
        let spec = CommentSpec::new("LegacyConfig".into(), ItemKind::Struct)
            .with_directive(SpecDirective::Allow(vec!["RL001".into(), "RL002".into()]));
        store.specs.insert("LegacyConfig".into(), spec);

        assert!(store.is_allowed("LegacyConfig", "RL001"));
        assert!(store.is_allowed("LegacyConfig", "RL002"));
        assert!(!store.is_allowed("LegacyConfig", "RL003"));
        assert!(!store.is_allowed("OtherStruct", "RL001"));
    }

    #[test]
    fn test_allow_store_wildcard() {
        let mut store = AllowStore::new();

        let spec = CommentSpec::new("LegacyModule".into(), ItemKind::Mod)
            .with_directive(SpecDirective::Allow(vec!["RL*".into()]));
        store.specs.insert("LegacyModule".into(), spec);

        assert!(store.is_allowed("LegacyModule", "RL001"));
        assert!(store.is_allowed("LegacyModule", "RL999"));
        assert!(!store.is_allowed("LegacyModule", "PT001"));
    }

    #[test]
    fn test_allow_store_empty() {
        let store = AllowStore::new();

        assert!(!store.is_allowed("AnyStruct", "RL001"));
        assert!(store.is_empty());
    }
}