Skip to main content

ryo_suggest/
allow.rs

1//! Allow filtering for suggestions based on @spec:allow directives.
2//!
3//! This module provides functionality to check if a suggestion should be
4//! skipped based on `@spec:allow(...)` directives in doc comments.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! /// @spec:allow(RL001, RL002)
10//! struct LegacyConfig {
11//!     // This struct will not trigger RL001 or RL002 suggestions
12//! }
13//! ```
14
15use std::collections::HashMap;
16
17use ryo_analysis::context::AnalysisContext;
18use ryo_analysis::symbol::WorkspaceFilePath;
19use ryo_analysis::SymbolId;
20use ryo_source::pure::PureFile;
21use ryo_spec::comment::{CommentSpec, CommentSpecExtractor};
22
23/// Store for allow directives extracted from source files.
24///
25/// Caches CommentSpec information per symbol name for efficient lookups
26/// during suggestion filtering.
27#[derive(Debug, Default)]
28pub struct AllowStore {
29    /// Map from symbol name to its CommentSpec (contains allow directives)
30    specs: HashMap<String, CommentSpec>,
31}
32
33impl AllowStore {
34    /// Create a new empty AllowStore.
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Build AllowStore from AnalysisContext.
40    ///
41    /// Extracts all CommentSpec directives from all files in the context.
42    pub fn from_context(ctx: &AnalysisContext) -> Self {
43        let mut store = Self::new();
44        let extractor = CommentSpecExtractor::new();
45
46        for (path, file) in ctx.files() {
47            store.extract_from_file(path, file, &extractor);
48        }
49
50        store
51    }
52
53    /// Extract allow directives from a single file.
54    fn extract_from_file(
55        &mut self,
56        _path: &WorkspaceFilePath,
57        file: &PureFile,
58        extractor: &CommentSpecExtractor,
59    ) {
60        let specs = extractor.extract(file);
61        for spec in specs {
62            // Store by target name (e.g., "LegacyConfig")
63            self.specs.insert(spec.target.clone(), spec);
64        }
65    }
66
67    /// Check if a rule is allowed for a specific symbol.
68    ///
69    /// Returns true if the rule should be skipped (i.e., is allowed).
70    pub fn is_allowed(&self, symbol_name: &str, rule_id: &str) -> bool {
71        if let Some(spec) = self.specs.get(symbol_name) {
72            spec.is_rule_allowed(rule_id)
73        } else {
74            false
75        }
76    }
77
78    /// Check if a rule is allowed for any of the given symbol IDs.
79    ///
80    /// Looks up symbol names from the registry and checks allow directives.
81    pub fn is_allowed_for_symbols(
82        &self,
83        ctx: &AnalysisContext,
84        symbol_ids: &[SymbolId],
85        rule_id: &str,
86    ) -> bool {
87        for &symbol_id in symbol_ids {
88            if let Some(path) = ctx.registry.path(symbol_id) {
89                // Get the symbol name (last component of the path)
90                let symbol_name = path.name();
91                if self.is_allowed(symbol_name, rule_id) {
92                    return true;
93                }
94            }
95        }
96        false
97    }
98
99    /// Get the CommentSpec for a symbol if it exists.
100    pub fn get(&self, symbol_name: &str) -> Option<&CommentSpec> {
101        self.specs.get(symbol_name)
102    }
103
104    /// Get the number of specs in the store.
105    pub fn len(&self) -> usize {
106        self.specs.len()
107    }
108
109    /// Check if the store is empty.
110    pub fn is_empty(&self) -> bool {
111        self.specs.is_empty()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use ryo_source::ItemKind;
119    use ryo_spec::comment::SpecDirective;
120
121    #[test]
122    fn test_allow_store_basic() {
123        let mut store = AllowStore::new();
124
125        // Manually add a spec with allow directive
126        let spec = CommentSpec::new("LegacyConfig".into(), ItemKind::Struct)
127            .with_directive(SpecDirective::Allow(vec!["RL001".into(), "RL002".into()]));
128        store.specs.insert("LegacyConfig".into(), spec);
129
130        assert!(store.is_allowed("LegacyConfig", "RL001"));
131        assert!(store.is_allowed("LegacyConfig", "RL002"));
132        assert!(!store.is_allowed("LegacyConfig", "RL003"));
133        assert!(!store.is_allowed("OtherStruct", "RL001"));
134    }
135
136    #[test]
137    fn test_allow_store_wildcard() {
138        let mut store = AllowStore::new();
139
140        let spec = CommentSpec::new("LegacyModule".into(), ItemKind::Mod)
141            .with_directive(SpecDirective::Allow(vec!["RL*".into()]));
142        store.specs.insert("LegacyModule".into(), spec);
143
144        assert!(store.is_allowed("LegacyModule", "RL001"));
145        assert!(store.is_allowed("LegacyModule", "RL999"));
146        assert!(!store.is_allowed("LegacyModule", "PT001"));
147    }
148
149    #[test]
150    fn test_allow_store_empty() {
151        let store = AllowStore::new();
152
153        assert!(!store.is_allowed("AnyStruct", "RL001"));
154        assert!(store.is_empty());
155    }
156}