ryo-analysis 0.1.0

Code graph and discovery engine for the RYO project
Documentation
//! DiscoveryQuery - Query specification for symbol discovery.

use crate::pattern::CaseOptions;
use crate::query::UsageContext;
use crate::Pattern;
use crate::SymbolKind;
use serde::{Deserialize, Serialize};

/// Sort order for discovery results.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum SortOrder {
    /// Sort by reference count (most used first) - default
    #[default]
    Refs,
    /// Alphabetical order
    Alpha,
    /// Sort by impl count (find root traits/important types)
    Impls,
}

// =============================================================================
// TypeFilter - TypeFlowGraph-based filtering
// =============================================================================

/// Type-based filter using TypeFlowGraph.
///
/// Filters symbols by their type usage contexts (return type, parameter type, etc.).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TypeFilter {
    /// Filter by return type pattern.
    /// Matches functions that return types matching this pattern.
    pub return_type: Option<Pattern>,

    /// Filter by parameter type pattern.
    /// Matches functions that have parameters of types matching this pattern.
    pub param_type: Option<Pattern>,

    /// Filter by field type pattern.
    /// Matches structs/enums with fields of types matching this pattern.
    pub field_type: Option<Pattern>,

    /// Filter by any usage context pattern.
    /// Matches any symbol that uses types matching this pattern.
    pub uses_type: Option<Pattern>,

    /// Filter by trait bound pattern.
    /// Matches symbols with generic parameters bounded by matching traits.
    pub has_bound: Option<Pattern>,
}

impl TypeFilter {
    /// Create a filter for functions returning a specific type.
    pub fn returns(pattern: impl Into<String>) -> Self {
        Self {
            return_type: Some(Pattern::glob(pattern.into())),
            ..Default::default()
        }
    }

    /// Create a filter for functions with a specific parameter type.
    pub fn has_param(pattern: impl Into<String>) -> Self {
        Self {
            param_type: Some(Pattern::glob(pattern.into())),
            ..Default::default()
        }
    }

    /// Create a filter for types with a specific field type.
    pub fn has_field(pattern: impl Into<String>) -> Self {
        Self {
            field_type: Some(Pattern::glob(pattern.into())),
            ..Default::default()
        }
    }

    /// Create a filter for symbols using a specific type.
    pub fn uses(pattern: impl Into<String>) -> Self {
        Self {
            uses_type: Some(Pattern::glob(pattern.into())),
            ..Default::default()
        }
    }

    /// Add trait bound filter.
    pub fn with_bound(mut self, pattern: impl Into<String>) -> Self {
        self.has_bound = Some(Pattern::glob(pattern.into()));
        self
    }

    /// Check if any filter is set.
    pub fn is_empty(&self) -> bool {
        self.return_type.is_none()
            && self.param_type.is_none()
            && self.field_type.is_none()
            && self.uses_type.is_none()
            && self.has_bound.is_none()
    }

    /// Get the expected usage contexts based on set filters.
    pub fn expected_contexts(&self) -> Vec<UsageContext> {
        let mut contexts = Vec::new();
        if self.return_type.is_some() {
            contexts.push(UsageContext::ReturnType);
        }
        if self.param_type.is_some() {
            contexts.push(UsageContext::ParamType);
        }
        if self.field_type.is_some() {
            contexts.push(UsageContext::FieldType);
            contexts.push(UsageContext::VariantField);
        }
        contexts
    }
}

/// Query specification for symbol discovery.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoveryQuery {
    /// Name pattern to match.
    pub pattern: Pattern,
    /// Case matching options.
    #[serde(default, skip_serializing_if = "is_default_case_options")]
    pub case_options: CaseOptions,
    /// Filter by kinds.
    pub kinds: Option<Vec<SymbolKind>>,
    /// Filter by crate.
    pub in_crate: Option<String>,
    /// Filter by module path (partial match).
    pub in_module: Option<String>,
    /// Include related symbols (callers, implementors, etc.).
    pub include_relations: bool,
    /// Maximum depth for relation traversal.
    pub relation_depth: usize,
    /// Maximum results to return.
    pub limit: Option<usize>,
    /// Sort order for results.
    pub sort: SortOrder,
    /// Type-based filter using TypeFlowGraph.
    pub type_filter: Option<TypeFilter>,
}

fn is_default_case_options(opts: &CaseOptions) -> bool {
    opts.is_default()
}

impl DiscoveryQuery {
    /// Create a query matching symbol names.
    pub fn symbol(pattern: impl Into<String>) -> Self {
        Self {
            pattern: Pattern::glob(pattern.into()),
            case_options: CaseOptions::default(),
            kinds: None,
            in_crate: None,
            in_module: None,
            include_relations: false,
            relation_depth: 1,
            limit: None,
            sort: SortOrder::default(),
            type_filter: None,
        }
    }

    /// Create a query matching symbol names with case options.
    pub fn symbol_with_options(pattern: impl Into<String>, case_options: CaseOptions) -> Self {
        Self {
            pattern: Pattern::glob_with_options(pattern.into(), case_options),
            case_options,
            kinds: None,
            in_crate: None,
            in_module: None,
            include_relations: false,
            relation_depth: 1,
            limit: None,
            sort: SortOrder::default(),
            type_filter: None,
        }
    }

    /// Create a query with exact name match.
    pub fn exact(name: impl Into<String>) -> Self {
        Self {
            pattern: Pattern::exact(name.into()),
            case_options: CaseOptions::default(),
            kinds: None,
            in_crate: None,
            in_module: None,
            include_relations: false,
            relation_depth: 1,
            limit: None,
            sort: SortOrder::default(),
            type_filter: None,
        }
    }

    /// Create a query with regex pattern.
    pub fn regex(pattern: impl Into<String>) -> Result<Self, crate::pattern::PatternError> {
        Ok(Self {
            pattern: Pattern::regex(pattern.into())?,
            case_options: CaseOptions::default(),
            kinds: None,
            in_crate: None,
            in_module: None,
            include_relations: false,
            relation_depth: 1,
            limit: None,
            sort: SortOrder::default(),
            type_filter: None,
        })
    }

    /// Enable case-insensitive matching.
    ///
    /// # Examples
    /// ```ignore
    /// let query = DiscoveryQuery::symbol("*config").ignore_case();
    /// // Matches: AppConfig, APPCONFIG, appconfig
    /// ```
    pub fn ignore_case(mut self) -> Self {
        self.case_options.ignore_case = true;
        // Rebuild pattern with new case options
        let pattern_str = self.pattern.as_str().to_string();
        self.pattern = if self.pattern.is_glob() {
            Pattern::glob_with_options(pattern_str, self.case_options)
        } else if self.pattern.is_regex() {
            Pattern::regex_with_options(pattern_str, self.case_options)
                .unwrap_or_else(|_| self.pattern.clone())
        } else {
            Pattern::exact_with_options(pattern_str, self.case_options)
        };
        self
    }

    /// Enable word-separate-insensitive matching.
    ///
    /// Ignores differences in casing style (snake_case vs camelCase vs PascalCase).
    ///
    /// # Examples
    /// ```ignore
    /// let query = DiscoveryQuery::symbol("get_user*").ignore_word_separate();
    /// // Matches: get_user_name, getUserName, GetUserName
    /// ```
    pub fn ignore_word_separate(mut self) -> Self {
        self.case_options.ignore_word_separate = true;
        // Rebuild pattern with new case options
        let pattern_str = self.pattern.as_str().to_string();
        self.pattern = if self.pattern.is_glob() {
            Pattern::glob_with_options(pattern_str, self.case_options)
        } else if self.pattern.is_regex() {
            // Regex doesn't support word-separate, keep as-is
            self.pattern.clone()
        } else {
            Pattern::exact_with_options(pattern_str, self.case_options)
        };
        self
    }

    /// Filter by specific kinds.
    pub fn kinds(mut self, kinds: Vec<SymbolKind>) -> Self {
        self.kinds = Some(kinds);
        self
    }

    /// Filter by a single kind.
    pub fn kind(mut self, kind: SymbolKind) -> Self {
        self.kinds = Some(vec![kind]);
        self
    }

    /// Filter by crate name.
    pub fn in_crate(mut self, crate_name: impl Into<String>) -> Self {
        self.in_crate = Some(crate_name.into());
        self
    }

    /// Filter by module path.
    pub fn in_module(mut self, module: impl Into<String>) -> Self {
        self.in_module = Some(module.into());
        self
    }

    /// Include related symbols in results.
    pub fn with_relations(mut self) -> Self {
        self.include_relations = true;
        self
    }

    /// Set relation traversal depth.
    pub fn relation_depth(mut self, depth: usize) -> Self {
        self.relation_depth = depth;
        self
    }

    /// Limit the number of results.
    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = Some(limit);
        self
    }

    /// Set sort order.
    pub fn sort(mut self, sort: SortOrder) -> Self {
        self.sort = sort;
        self
    }

    /// Add type-based filter.
    pub fn with_type_filter(mut self, filter: TypeFilter) -> Self {
        self.type_filter = Some(filter);
        self
    }

    /// Filter by return type pattern.
    pub fn returns(self, pattern: impl Into<String>) -> Self {
        self.with_type_filter(TypeFilter::returns(pattern))
    }

    /// Filter by parameter type pattern.
    pub fn has_param_type(self, pattern: impl Into<String>) -> Self {
        self.with_type_filter(TypeFilter::has_param(pattern))
    }

    /// Filter by field type pattern.
    pub fn has_field_type(self, pattern: impl Into<String>) -> Self {
        self.with_type_filter(TypeFilter::has_field(pattern))
    }
}

impl Default for DiscoveryQuery {
    fn default() -> Self {
        Self::symbol("*")
    }
}

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

    #[test]
    fn test_symbol_query() {
        let query = DiscoveryQuery::symbol("*Config");
        assert!(query.pattern.matches("AppConfig"));
        assert!(query.kinds.is_none());
    }

    #[test]
    fn test_exact_query() {
        let query = DiscoveryQuery::exact("Config");
        assert!(query.pattern.matches("Config"));
        assert!(!query.pattern.matches("AppConfig"));
    }

    #[test]
    fn test_query_with_kinds() {
        let query = DiscoveryQuery::symbol("*").kinds(vec![SymbolKind::Struct, SymbolKind::Enum]);
        assert_eq!(query.kinds.as_ref().unwrap().len(), 2);
    }

    #[test]
    fn test_query_chaining() {
        let query = DiscoveryQuery::symbol("*Handler")
            .kind(SymbolKind::Struct)
            .in_crate("mylib")
            .in_module("handlers")
            .with_relations()
            .limit(10);

        assert!(query.kinds.is_some());
        assert_eq!(query.in_crate, Some("mylib".to_string()));
        assert_eq!(query.in_module, Some("handlers".to_string()));
        assert!(query.include_relations);
        assert_eq!(query.limit, Some(10));
    }
}