ryo-suggest 0.1.0

[experimental] Pattern-based suggestion engine for RYO
Documentation
//! InvalidSpecRelation - Detects SpecRelation targets that don't exist
//!
//! This rule validates that all SpecRelation targets reference existing types.

use ryo_analysis::context::AnalysisContext;
use ryo_analysis::{SymbolId, SymbolKind};

use super::{is_framework_type, SpecSuggest};
use crate::lint::{LintDetails, LintSuggest};
use crate::{
    LintSeverity, MutationSpec, OpportunityId, SafetyLevel, Suggest, SuggestCategory,
    SuggestLocation, SuggestOpportunity, SuggestResult,
};

/// InvalidSpecRelation rule
///
/// Detects SpecRelation targets that don't exist in the codebase.
///
/// # Rule Code
/// RS003 (Ryo Spec)
///
/// # Detection
/// 1. Find TypeAliases matching `*Spec` pattern
/// 2. Parse the type definition to extract referenced types
/// 3. Check if referenced types exist in the codebase
/// 4. Report missing type references
///
/// # Example Violation
/// ```ignore
/// // src/domain/task.rs
/// type TaskSpec = Spec<DomainGroup, Task, Relations![
///     DependsOn(NonExistentType)  // ERROR: NonExistentType doesn't exist
/// ]>;
/// ```
///
/// # Fix
/// Generates `ValidateSpec` MutationSpec to validate and report issues.
pub struct InvalidSpecRelation {
    /// Suffix pattern to identify Spec TypeAliases (default: "Spec")
    spec_suffix: String,
}

impl InvalidSpecRelation {
    pub fn new() -> Self {
        Self {
            spec_suffix: "Spec".to_string(),
        }
    }

    /// Create with custom suffix pattern
    pub fn with_suffix(suffix: impl Into<String>) -> Self {
        Self {
            spec_suffix: suffix.into(),
        }
    }

    /// Check if a type exists in the codebase
    fn type_exists(&self, ctx: &AnalysisContext, type_name: &str) -> bool {
        // Search for Struct or Enum with matching name
        for (id, path) in ctx.registry.iter() {
            let kind = ctx.registry.kind(id);
            if matches!(kind, Some(SymbolKind::Struct) | Some(SymbolKind::Enum))
                && path.name() == type_name
            {
                return true;
            }
        }
        false
    }

    /// Extract types referenced in a Spec TypeAlias definition
    ///
    /// This is a heuristic approach that looks for type references
    /// in the TypeAlias definition by analyzing the AST.
    fn extract_referenced_types(&self, ctx: &AnalysisContext, symbol_id: SymbolId) -> Vec<String> {
        let mut referenced_types = Vec::new();

        // Get types used by this symbol
        let typeflow = ctx.typeflow_graph();
        for used_id in typeflow.types_used_by(symbol_id) {
            if let Some(path) = ctx.registry.path(used_id) {
                let kind = ctx.registry.kind(used_id);
                if matches!(kind, Some(SymbolKind::Struct) | Some(SymbolKind::Enum)) {
                    referenced_types.push(path.name().to_string());
                }
            }
        }

        referenced_types
    }

    /// Validate that a Spec's base type exists
    fn validate_base_type(&self, ctx: &AnalysisContext, alias_name: &str) -> Option<String> {
        let base_type = self.extract_base_type(alias_name)?;
        if !self.type_exists(ctx, base_type) {
            Some(base_type.to_string())
        } else {
            None
        }
    }
}

impl SpecSuggest for InvalidSpecRelation {
    fn spec_suffix(&self) -> &str {
        &self.spec_suffix
    }
}

impl Default for InvalidSpecRelation {
    fn default() -> Self {
        Self::new()
    }
}

impl Suggest for InvalidSpecRelation {
    fn name(&self) -> &'static str {
        "invalid-spec-relation"
    }

    fn description(&self) -> &str {
        "Detects SpecRelation targets that don't exist in the codebase"
    }

    fn category(&self) -> SuggestCategory {
        SuggestCategory::Lint
    }

    fn safety_level(&self) -> SafetyLevel {
        SafetyLevel::Manual // Requires manual investigation and fix
    }

    fn priority_weight(&self) -> f32 {
        2.0 // High priority - invalid references are errors
    }

    fn detect(&self, ctx: &AnalysisContext, symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
        let mut opportunities = Vec::new();
        let mut next_id = 0u32;

        // If specific symbols provided, check only those
        let symbols_to_check: Vec<SymbolId> = if symbols.is_empty() {
            ctx.registry.iter_by_kind(SymbolKind::TypeAlias).collect()
        } else {
            symbols.to_vec()
        };

        for symbol_id in symbols_to_check {
            let path = match ctx.registry.path(symbol_id) {
                Some(p) => p,
                None => continue,
            };

            let alias_name = path.name();

            // Check if this looks like a Spec TypeAlias
            if !self.is_spec_alias(alias_name) {
                continue;
            }

            // Check 1: Validate base type exists
            if let Some(missing_base) = self.validate_base_type(ctx, alias_name) {
                let Some(location) = SuggestLocation::from_context(ctx, symbol_id) else {
                    continue;
                };

                let opp = self.create_lint_opportunity(
                    OpportunityId::new(next_id),
                    vec![symbol_id],
                    location,
                    format!(
                        "Spec `{}` references non-existent type `{}`",
                        alias_name, missing_base
                    ),
                    LintDetails {
                        suggestion: Some(format!(
                            "Create type `{}` or update the Spec definition",
                            missing_base
                        )),
                        expected: Some(format!("Type `{}` exists", missing_base)),
                        actual: Some(format!("Type `{}` not found", missing_base)),
                    },
                );

                opportunities.push(opp);
                next_id += 1;
                continue; // Don't check relations if base type is invalid
            }

            // Check 2: Validate referenced types exist
            let referenced = self.extract_referenced_types(ctx, symbol_id);
            for ref_type in &referenced {
                // Skip checking the base type (already validated)
                if let Some(base) = self.extract_base_type(alias_name) {
                    if ref_type == base {
                        continue;
                    }
                }

                // Skip common framework types
                if is_framework_type(ref_type) {
                    continue;
                }

                if !self.type_exists(ctx, ref_type) {
                    let Some(location) = SuggestLocation::from_context(ctx, symbol_id) else {
                        continue;
                    };

                    let opp = self.create_lint_opportunity(
                        OpportunityId::new(next_id),
                        vec![symbol_id],
                        location,
                        format!(
                            "Spec `{}` has relation to non-existent type `{}`",
                            alias_name, ref_type
                        ),
                        LintDetails {
                            suggestion: Some(format!(
                                "Create type `{}` or remove the relation",
                                ref_type
                            )),
                            expected: Some(format!("Type `{}` exists", ref_type)),
                            actual: Some(format!("Type `{}` not found", ref_type)),
                        },
                    );

                    opportunities.push(opp);
                    next_id += 1;
                }
            }
        }

        opportunities
    }

    fn to_mutation_specs(
        &self,
        ctx: &AnalysisContext,
        opportunity: &SuggestOpportunity,
    ) -> SuggestResult<Vec<MutationSpec>> {
        // Get the target symbol (Spec TypeAlias)
        let symbol_id = match opportunity.targets.first() {
            Some(id) => *id,
            None => return Ok(Vec::new()),
        };

        let path = match ctx.registry.path(symbol_id) {
            Some(p) => p,
            None => return Ok(Vec::new()),
        };

        let _module_path = self.get_module_path(path);

        // Generate ValidateSpec to validate and report the issue
        Ok(vec![MutationSpec::ValidateSpec {
            type_ids: vec![symbol_id],
            expected_group: None,
            validate_relations: true,
        }])
    }
}

impl LintSuggest for InvalidSpecRelation {
    fn code(&self) -> &'static str {
        "RS003"
    }

    fn default_severity(&self) -> LintSeverity {
        LintSeverity::Error // Invalid references are errors
    }
}

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

    #[test]
    fn test_is_spec_alias() {
        let rule = InvalidSpecRelation::new();

        assert!(rule.is_spec_alias("TaskSpec"));
        assert!(rule.is_spec_alias("UserSpec"));
        assert!(rule.is_spec_alias("ConfigSpec"));

        assert!(!rule.is_spec_alias("Spec"));
        assert!(!rule.is_spec_alias("Task"));
        assert!(!rule.is_spec_alias("SpecTask"));
    }

    #[test]
    fn test_extract_base_type() {
        let rule = InvalidSpecRelation::new();

        assert_eq!(rule.extract_base_type("TaskSpec"), Some("Task"));
        assert_eq!(rule.extract_base_type("UserSpec"), Some("User"));
        assert_eq!(rule.extract_base_type("Spec"), None);
        assert_eq!(rule.extract_base_type("Task"), None);
    }

    #[test]
    fn test_custom_suffix() {
        let rule = InvalidSpecRelation::with_suffix("Domain");

        assert!(rule.is_spec_alias("TaskDomain"));
        assert!(!rule.is_spec_alias("TaskSpec"));

        assert_eq!(rule.extract_base_type("TaskDomain"), Some("Task"));
    }

    #[test]
    fn test_is_framework_type() {
        assert!(is_framework_type("Spec"));
        assert!(is_framework_type("DomainGroup"));
        assert!(is_framework_type("String"));
        assert!(is_framework_type("Vec"));
        assert!(is_framework_type("u64"));

        assert!(!is_framework_type("Task"));
        assert!(!is_framework_type("User"));
        assert!(!is_framework_type("MyCustomType"));
    }
}