diff --git a/src/core/mod.rs b/src/core/mod.rs index bea636b..8af41d8 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,2 +1,3 @@ pub mod parser; pub mod scanner; +pub mod semantic_analyzer; diff --git a/src/core/semantic_analyzer/context.rs b/src/core/semantic_analyzer/context.rs new file mode 100644 index 0000000..d19c757 --- /dev/null +++ b/src/core/semantic_analyzer/context.rs @@ -0,0 +1,593 @@ +//! Analysis context and result types. +//! +//! This module provides the shared state and result types used throughout +//! the semantic analysis pipeline. + +use crate::core::semantic_analyzer::{ + AnalyzerOptions, diagnostics::SemanticDiagnostic, + symbol_table::SymbolTable, type_resolver::TypeResolver, +}; + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; + +/// Shared context for semantic analysis phases. +/// +/// The analysis context provides shared state, utilities, and metadata +/// collection for all analyzers in the pipeline. State is shared via +/// thread-safe reference counting and read-write locks to support both +/// sequential and parallel execution. +#[derive(Debug)] +pub struct AnalysisContext { + options: AnalyzerOptions, + + /// Shared symbol table across all phases + pub symbol_table: Arc>, + + /// Shared type resolver across all phases + pub type_resolver: Arc>, + + /// Shared relationship graph across all phases + pub relationship_graph: Arc>, + + /// Current scope stack for symbol resolution + current_scope: ScopeStack, + + /// Analysis metadata collection + metadata: AnalysisMetadata, + + /// Start time for timeout tracking + start_time: Instant, + + /// Current analysis state + current_model: Option, + current_field: Option, + current_enum: Option, + + /// Stack for tracking type resolution to detect cycles + type_resolution_stack: Vec, +} + +/// Relationship graph for tracking model relationships. +#[derive(Debug, Clone, Default)] +pub struct RelationshipGraph { + /// All relationships indexed by ID + pub relationships: HashMap, + + /// Relationships by source model + pub model_relationships: HashMap>, +} + +/// Represents a relationship between two models. +#[derive(Debug, Clone)] +pub struct Relationship { + pub id: String, + pub from_model: String, + pub from_field: String, + pub to_model: String, + pub to_field: Option, + pub relationship_type: RelationshipType, + pub foreign_keys: Vec, + pub references: Vec, + /// Source span for the relationship (attribute or field) + pub span: crate::core::scanner::tokens::SymbolSpan, +} + +/// Type of relationship between models. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RelationshipType { + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, +} + +impl AnalysisContext { + /// Create a new analysis context with shared state. + #[must_use] + pub fn new( + options: &AnalyzerOptions, + symbol_table: Arc>, + type_resolver: Arc>, + relationship_graph: Arc>, + ) -> Self { + Self { + options: options.clone(), + symbol_table, + type_resolver, + relationship_graph, + current_scope: ScopeStack::new(), + metadata: AnalysisMetadata::new(), + start_time: Instant::now(), + current_model: None, + current_field: None, + current_enum: None, + type_resolution_stack: Vec::new(), + } + } + + /// Create a test context with default shared state for unit tests. + #[cfg(test)] + #[must_use] + pub fn new_test(options: &AnalyzerOptions) -> Self { + Self::new( + options, + Arc::new(RwLock::new(SymbolTable::new())), + Arc::new(RwLock::new(TypeResolver::new())), + Arc::new(RwLock::new(RelationshipGraph::default())), + ) + } + + /// Get the analysis options. + #[must_use] + pub fn options(&self) -> &AnalyzerOptions { + &self.options + } + + // Note: Symbol table access will be handled by the analyzer orchestrator + // to avoid unsafe code. Individual analyzers receive symbol table references + // as method parameters. + + /// Enter a new scope. + pub fn enter_scope(&mut self, scope_type: ScopeType, name: String) { + self.current_scope.push(Scope { scope_type, name }); + } + + /// Exit the current scope. + pub fn exit_scope(&mut self) -> Option { + self.current_scope.pop() + } + + /// Get the current scope. + #[must_use] + pub fn current_scope(&self) -> Option<&Scope> { + self.current_scope.current() + } + + /// Set the current model being analyzed. + pub fn set_current_model(&mut self, model_name: Option) { + self.current_model = model_name; + } + + /// Get the current model being analyzed. + #[must_use] + pub fn current_model(&self) -> Option<&str> { + self.current_model.as_deref() + } + + /// Set the current field being analyzed. + pub fn set_current_field(&mut self, field_name: Option) { + self.current_field = field_name; + } + + /// Get the current field being analyzed. + #[must_use] + pub fn current_field(&self) -> Option<&str> { + self.current_field.as_deref() + } + + /// Set the current enum being analyzed. + pub fn set_current_enum(&mut self, enum_name: Option) { + self.current_enum = enum_name; + } + + /// Get the current enum being analyzed. + #[must_use] + pub fn current_enum(&self) -> Option<&str> { + self.current_enum.as_deref() + } + + /// Push a type onto the resolution stack (for cycle detection). + pub fn push_type_resolution(&mut self, type_name: String) { + self.type_resolution_stack.push(type_name); + } + + /// Pop a type from the resolution stack. + pub fn pop_type_resolution(&mut self) -> Option { + self.type_resolution_stack.pop() + } + + /// Check if a type is currently being resolved (cycle detection). + #[must_use] + pub fn is_resolving_type(&self, type_name: &str) -> bool { + self.type_resolution_stack.contains(&type_name.to_string()) + } + + /// Get the current type resolution stack. + #[must_use] + pub fn type_resolution_stack(&self) -> &[String] { + &self.type_resolution_stack + } + + /// Record metadata about the analysis. + pub fn record_metadata( + &mut self, + key: String, + value: AnalysisMetadataValue, + ) { + self.metadata.add_entry(key, value); + } + + /// Get elapsed time since analysis started. + #[must_use] + pub fn elapsed_time(&self) -> Duration { + self.start_time.elapsed() + } + + /// Check if analysis has timed out. + #[must_use] + pub fn has_timed_out(&self) -> bool { + self.elapsed_time() > self.options.phase_timeout + } + + /// Record per-phase timing into analysis metadata. + pub fn record_phase_timing( + &mut self, + phase_name: String, + duration: Duration, + ) { + self.metadata.record_phase_timing(phase_name, duration); + } + + /// Take the analysis metadata (consuming it). + #[must_use] + pub fn take_metadata(self) -> AnalysisMetadata { + self.metadata + } + + // Symbol resolution will be handled by passing symbol table to methods +} + +/// Stack for tracking analysis scopes. +#[derive(Debug, Clone)] +pub struct ScopeStack { + scopes: Vec, +} + +impl Default for ScopeStack { + fn default() -> Self { + Self::new() + } +} + +impl ScopeStack { + #[must_use] + pub fn new() -> Self { + Self { scopes: Vec::new() } + } + + pub fn push(&mut self, scope: Scope) { + self.scopes.push(scope); + } + + pub fn pop(&mut self) -> Option { + self.scopes.pop() + } + + #[must_use] + pub fn current(&self) -> Option<&Scope> { + self.scopes.last() + } + + #[must_use] + pub fn depth(&self) -> usize { + self.scopes.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.scopes.is_empty() + } +} + +/// A single scope in the scope stack. +#[derive(Debug, Clone)] +pub struct Scope { + pub scope_type: ScopeType, + pub name: String, +} + +/// Types of scopes in semantic analysis. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScopeType { + Global, + Model, + Enum, + Datasource, + Generator, + Field, + EnumValue, +} + +/// Result of symbol resolution. +#[derive(Debug, Clone)] +pub struct SymbolResolution { + pub symbol: crate::core::semantic_analyzer::symbol_table::Symbol, + pub scope: String, +} + +/// Metadata collected during analysis. +#[derive(Debug, Clone)] +pub struct AnalysisMetadata { + entries: HashMap, + phase_timings: HashMap, + statistics: AnalysisStatistics, +} + +impl Default for AnalysisMetadata { + fn default() -> Self { + Self::new() + } +} + +impl AnalysisMetadata { + #[must_use] + pub fn new() -> Self { + Self { + entries: HashMap::new(), + phase_timings: HashMap::new(), + statistics: AnalysisStatistics::default(), + } + } + + pub fn add_entry(&mut self, key: String, value: AnalysisMetadataValue) { + self.entries.insert(key, value); + } + + #[must_use] + pub fn get_entry(&self, key: &str) -> Option<&AnalysisMetadataValue> { + self.entries.get(key) + } + + pub fn record_phase_timing( + &mut self, + phase_name: String, + duration: Duration, + ) { + self.phase_timings.insert(phase_name, duration); + } + + #[must_use] + pub fn get_phase_timing(&self, phase_name: &str) -> Option { + self.phase_timings.get(phase_name).copied() + } + + #[must_use] + pub fn statistics(&self) -> &AnalysisStatistics { + &self.statistics + } + + pub fn statistics_mut(&mut self) -> &mut AnalysisStatistics { + &mut self.statistics + } + + #[must_use] + pub fn total_analysis_time(&self) -> Duration { + self.phase_timings.values().sum() + } +} + +/// Values that can be stored in analysis metadata. +#[derive(Debug, Clone, PartialEq)] +pub enum AnalysisMetadataValue { + String(String), + Integer(i64), + Float(f64), + Boolean(bool), + Duration(Duration), + StringList(Vec), +} + +/// Statistics collected during analysis. +#[derive(Debug, Clone, Default)] +pub struct AnalysisStatistics { + pub models_analyzed: usize, + pub enums_analyzed: usize, + pub fields_analyzed: usize, + pub relationships_found: usize, + pub attributes_validated: usize, + pub type_resolutions: usize, + pub circular_dependencies_detected: usize, + pub warnings_generated: usize, + pub errors_generated: usize, +} + +/// Result from a single analysis phase. +#[derive(Debug, Clone)] +pub struct PhaseResult { + /// Diagnostics generated by this phase + pub diagnostics: Vec, + + /// Whether this phase completed successfully + pub success: bool, + + /// Time taken by this phase + pub duration: Option, + + /// Phase-specific metadata + pub metadata: HashMap, +} + +impl PhaseResult { + /// Create a new successful phase result with diagnostics. + #[must_use] + pub fn new(diagnostics: Vec) -> Self { + Self { + success: !diagnostics + .iter() + .any(super::diagnostics::SemanticDiagnostic::is_error), + diagnostics, + duration: None, + metadata: HashMap::new(), + } + } + + /// Create a successful phase result with no diagnostics. + #[must_use] + pub fn success() -> Self { + Self::new(Vec::new()) + } + + /// Create a failed phase result with an error diagnostic. + #[must_use] + pub fn error(diagnostic: SemanticDiagnostic) -> Self { + Self { + success: false, + diagnostics: vec![diagnostic], + duration: None, + metadata: HashMap::new(), + } + } + + /// Add timing information to this result. + #[must_use] + pub fn with_duration(mut self, duration: Duration) -> Self { + self.duration = Some(duration); + self + } + + /// Add metadata to this result. + #[must_use] + pub fn with_metadata( + mut self, + key: String, + value: AnalysisMetadataValue, + ) -> Self { + self.metadata.insert(key, value); + self + } + + /// Check if this result has fatal errors that should stop analysis. + #[must_use] + pub fn has_fatal_errors(&self) -> bool { + self.diagnostics + .iter() + .any(super::diagnostics::SemanticDiagnostic::is_fatal) + } + + /// Get the number of errors in this result. + #[must_use] + pub fn error_count(&self) -> usize { + self.diagnostics.iter().filter(|d| d.is_error()).count() + } + + /// Get the number of warnings in this result. + #[must_use] + pub fn warning_count(&self) -> usize { + self.diagnostics.iter().filter(|d| d.is_warning()).count() + } +} + +/// Final result of semantic analysis. +#[derive(Debug, Clone)] +pub struct AnalysisResult { + /// The completed symbol table + pub symbol_table: SymbolTable, + + /// The type resolver with all resolved types + pub type_resolver: TypeResolver, + + /// The relationship graph built during analysis + pub relationship_graph: RelationshipGraph, + + /// All diagnostics from all phases + pub diagnostics: Vec, + + /// Analysis metadata and statistics + pub analysis_metadata: AnalysisMetadata, + + /// Total time taken for analysis + pub analysis_time: Duration, + + /// Number of analyzers that were run + pub analyzer_count: usize, +} + +impl AnalysisResult { + /// Check if the analysis was successful (no errors). + #[must_use] + pub fn is_success(&self) -> bool { + !self + .diagnostics + .iter() + .any(super::diagnostics::SemanticDiagnostic::is_error) + } + + /// Get the number of errors. + #[must_use] + pub fn error_count(&self) -> usize { + self.diagnostics.iter().filter(|d| d.is_error()).count() + } + + /// Get the number of warnings. + #[must_use] + pub fn warning_count(&self) -> usize { + self.diagnostics.iter().filter(|d| d.is_warning()).count() + } + + /// Get the number of informational diagnostics. + #[must_use] + pub fn info_count(&self) -> usize { + self.diagnostics.iter().filter(|d| d.is_info()).count() + } + + /// Get diagnostics by severity. + #[must_use] + pub fn diagnostics_by_severity( + &self, + severity: crate::core::semantic_analyzer::diagnostics::DiagnosticSeverity, + ) -> Vec<&SemanticDiagnostic> { + self.diagnostics + .iter() + .filter(|d| d.severity == severity) + .collect() + } + + /// Get a summary of the analysis results. + #[must_use] + pub fn summary(&self) -> AnalysisSummary { + AnalysisSummary { + success: self.is_success(), + error_count: self.error_count(), + warning_count: self.warning_count(), + info_count: self.info_count(), + total_diagnostics: self.diagnostics.len(), + analysis_time: self.analysis_time, + models_count: self.symbol_table.models().count(), + enums_count: self.symbol_table.enums().count(), + total_symbols: self.symbol_table.total_symbol_count(), + } + } +} + +/// Summary of analysis results. +#[derive(Debug, Clone)] +pub struct AnalysisSummary { + pub success: bool, + pub error_count: usize, + pub warning_count: usize, + pub info_count: usize, + pub total_diagnostics: usize, + pub analysis_time: Duration, + pub models_count: usize, + pub enums_count: usize, + pub total_symbols: usize, +} + +impl std::fmt::Display for AnalysisSummary { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Analysis {} in {:?}: {} models, {} enums, {} total symbols. {} errors, {} warnings, {} info.", + if self.success { "succeeded" } else { "failed" }, + self.analysis_time, + self.models_count, + self.enums_count, + self.total_symbols, + self.error_count, + self.warning_count, + self.info_count + ) + } +} diff --git a/src/core/semantic_analyzer/diagnostics.rs b/src/core/semantic_analyzer/diagnostics.rs new file mode 100644 index 0000000..5c3d0b9 --- /dev/null +++ b/src/core/semantic_analyzer/diagnostics.rs @@ -0,0 +1,775 @@ +//! Diagnostic system for semantic analysis. +//! +//! This module provides a rich diagnostic system with error codes, severity levels, +//! suggestions, fix hints, and related span tracking for comprehensive error reporting. + +use crate::core::scanner::tokens::SymbolSpan; +use std::collections::HashMap; + +/// A diagnostic message from semantic analysis. +#[derive(Debug, Clone)] +pub struct SemanticDiagnostic { + /// Severity level of this diagnostic + pub severity: DiagnosticSeverity, + + /// Source location where this diagnostic applies + pub span: SymbolSpan, + + /// Human-readable diagnostic message + pub message: String, + + /// Structured diagnostic code for programmatic handling + pub diagnostic_code: DiagnosticCode, + + /// Optional suggestion for fixing the issue + pub suggestion: Option, + + /// Optional fix hint with specific replacements + pub fix_hint: Option, + + /// Related spans that provide additional context + pub related_spans: Vec, + + /// Additional metadata about this diagnostic + pub metadata: DiagnosticMetadata, +} + +impl SemanticDiagnostic { + /// Create a new error diagnostic. + #[must_use] + pub fn error( + span: SymbolSpan, + message: String, + code: DiagnosticCode, + ) -> Self { + Self { + severity: DiagnosticSeverity::Error, + span, + message, + diagnostic_code: code, + suggestion: None, + fix_hint: None, + related_spans: Vec::new(), + metadata: DiagnosticMetadata::default(), + } + } + + /// Create a new warning diagnostic. + #[must_use] + pub fn warning( + span: SymbolSpan, + message: String, + code: DiagnosticCode, + ) -> Self { + Self { + severity: DiagnosticSeverity::Warning, + span, + message, + diagnostic_code: code, + suggestion: None, + fix_hint: None, + related_spans: Vec::new(), + metadata: DiagnosticMetadata::default(), + } + } + + /// Create a new info diagnostic. + #[must_use] + pub fn info( + span: SymbolSpan, + message: String, + code: DiagnosticCode, + ) -> Self { + Self { + severity: DiagnosticSeverity::Info, + span, + message, + diagnostic_code: code, + suggestion: None, + fix_hint: None, + related_spans: Vec::new(), + metadata: DiagnosticMetadata::default(), + } + } + + /// Add a suggestion to this diagnostic. + #[must_use] + pub fn with_suggestion(mut self, suggestion: String) -> Self { + self.suggestion = Some(suggestion); + self + } + + /// Add a fix hint to this diagnostic. + #[must_use] + pub fn with_fix_hint(mut self, fix_hint: FixHint) -> Self { + self.fix_hint = Some(fix_hint); + self + } + + /// Add a related span to this diagnostic. + #[must_use] + pub fn with_related_span( + mut self, + span: SymbolSpan, + message: String, + ) -> Self { + self.related_spans.push(RelatedSpan { span, message }); + self + } + + /// Add multiple related spans to this diagnostic. + #[must_use] + pub fn with_related_spans(mut self, spans: Vec) -> Self { + self.related_spans.extend(spans); + self + } + + /// Add metadata to this diagnostic. + #[must_use] + pub fn with_metadata(mut self, key: String, value: String) -> Self { + self.metadata.add(key, value); + self + } + + /// Check if this is an error diagnostic. + #[must_use] + pub fn is_error(&self) -> bool { + matches!(self.severity, DiagnosticSeverity::Error) + } + + /// Check if this is a warning diagnostic. + #[must_use] + pub fn is_warning(&self) -> bool { + matches!(self.severity, DiagnosticSeverity::Warning) + } + + /// Check if this is an info diagnostic. + #[must_use] + pub fn is_info(&self) -> bool { + matches!(self.severity, DiagnosticSeverity::Info) + } + + /// Check if this is a fatal error that should stop analysis. + #[must_use] + pub fn is_fatal(&self) -> bool { + self.is_error() + && matches!( + self.diagnostic_code, + DiagnosticCode::CircularDependency + | DiagnosticCode::InvalidSchemaStructure + | DiagnosticCode::InternalError + | DiagnosticCode::AnalysisTimeout + ) + } +} + +/// Severity levels for diagnostics. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum DiagnosticSeverity { + /// Informational messages + Info, + /// Warning messages that don't prevent schema use + Warning, + /// Error messages that prevent schema use + Error, +} + +impl std::fmt::Display for DiagnosticSeverity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DiagnosticSeverity::Info => write!(f, "info"), + DiagnosticSeverity::Warning => write!(f, "warning"), + DiagnosticSeverity::Error => write!(f, "error"), + } + } +} + +/// Structured diagnostic codes for programmatic handling. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DiagnosticCode { + // Symbol Resolution Errors + UndeclaredIdentifier, + DuplicateDeclaration, + InvalidShadowing, + ReservedWordUsed, + + // Type System Errors + TypeMismatch, + UnknownType, + CircularDependency, + InvalidTypeModifier, + InvalidTypeUsage, + TypeConstraintViolation, + ConstraintViolation, + IncompatibleTypes, + + // Relationship Errors + InvalidRelation, + MissingBackReference, + ConflictingRelations, + InvalidReferentialAction, + RelationshipCycle, + + // Attribute Errors + UnknownAttribute, + MissingRequiredAttribute, + ConflictingAttributes, + InvalidAttributeArgument, + AttributeNotApplicable, + + // Business Rule Violations + MissingPrimaryKey, + InvalidIndexDefinition, + DatabaseProviderMismatch, + DeprecatedFeature, + + // Performance Warnings + PerformanceWarning, + IndexSuggestion, + QueryOptimizationHint, + + // Schema Structure Issues + InvalidSchemaStructure, + EmptyModel, + ModelTooSmall, + ModelTooLarge, + UnusedDeclaration, + MissingField, + MissingDatasource, + + // Security and Best Practice Issues + SecuritySuggestion, + + // Naming and Convention Issues + InvalidIdentifier, + ReservedKeyword, + NamingConvention, + ExperimentalFeature, + + // Internal Errors + InternalError, + AnalysisTimeout, +} + +impl DiagnosticCode { + /// Get the string representation of this diagnostic code. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + DiagnosticCode::UndeclaredIdentifier => "E001", + DiagnosticCode::DuplicateDeclaration => "E002", + DiagnosticCode::InvalidShadowing => "E003", + DiagnosticCode::ReservedWordUsed => "E004", + DiagnosticCode::TypeMismatch => "E101", + DiagnosticCode::UnknownType => "E102", + DiagnosticCode::CircularDependency => "E103", + DiagnosticCode::InvalidTypeModifier => "E104", + DiagnosticCode::InvalidTypeUsage => "E105", + DiagnosticCode::TypeConstraintViolation => "E106", + DiagnosticCode::ConstraintViolation => "E107", + DiagnosticCode::IncompatibleTypes => "E108", + DiagnosticCode::InvalidRelation => "E201", + DiagnosticCode::MissingBackReference => "E202", + DiagnosticCode::ConflictingRelations => "E203", + DiagnosticCode::InvalidReferentialAction => "E204", + DiagnosticCode::RelationshipCycle => "E205", + DiagnosticCode::UnknownAttribute => "E301", + DiagnosticCode::MissingRequiredAttribute => "E302", + DiagnosticCode::ConflictingAttributes => "E303", + DiagnosticCode::InvalidAttributeArgument => "E304", + DiagnosticCode::AttributeNotApplicable => "E305", + DiagnosticCode::MissingPrimaryKey => "E401", + DiagnosticCode::InvalidIndexDefinition => "E402", + DiagnosticCode::DatabaseProviderMismatch => "E403", + DiagnosticCode::DeprecatedFeature => "W001", + DiagnosticCode::PerformanceWarning => "W101", + DiagnosticCode::IndexSuggestion => "W102", + DiagnosticCode::QueryOptimizationHint => "W103", + DiagnosticCode::InvalidSchemaStructure => "E501", + DiagnosticCode::EmptyModel => "W201", + DiagnosticCode::ModelTooSmall => "W202", + DiagnosticCode::ModelTooLarge => "W203", + DiagnosticCode::UnusedDeclaration => "W204", + DiagnosticCode::MissingField => "E504", + DiagnosticCode::MissingDatasource => "W505", + DiagnosticCode::SecuritySuggestion => "I001", + DiagnosticCode::InvalidIdentifier => "E601", + DiagnosticCode::ReservedKeyword => "E602", + DiagnosticCode::NamingConvention => "W603", + DiagnosticCode::ExperimentalFeature => "W604", + DiagnosticCode::InternalError => "E999", + DiagnosticCode::AnalysisTimeout => "E998", + } + } + + /// Get the category of this diagnostic code. + #[must_use] + pub fn category(self) -> DiagnosticCategory { + match self { + DiagnosticCode::UndeclaredIdentifier + | DiagnosticCode::DuplicateDeclaration + | DiagnosticCode::InvalidShadowing + | DiagnosticCode::ReservedWordUsed + | DiagnosticCode::InvalidIdentifier + | DiagnosticCode::ReservedKeyword + | DiagnosticCode::NamingConvention + | DiagnosticCode::ExperimentalFeature => { + DiagnosticCategory::SymbolResolution + } + + DiagnosticCode::TypeMismatch + | DiagnosticCode::UnknownType + | DiagnosticCode::CircularDependency + | DiagnosticCode::InvalidTypeModifier + | DiagnosticCode::InvalidTypeUsage + | DiagnosticCode::TypeConstraintViolation + | DiagnosticCode::ConstraintViolation + | DiagnosticCode::IncompatibleTypes => { + DiagnosticCategory::TypeSystem + } + + DiagnosticCode::InvalidRelation + | DiagnosticCode::MissingBackReference + | DiagnosticCode::ConflictingRelations + | DiagnosticCode::InvalidReferentialAction + | DiagnosticCode::RelationshipCycle => { + DiagnosticCategory::Relationships + } + + DiagnosticCode::UnknownAttribute + | DiagnosticCode::MissingRequiredAttribute + | DiagnosticCode::ConflictingAttributes + | DiagnosticCode::InvalidAttributeArgument + | DiagnosticCode::AttributeNotApplicable => { + DiagnosticCategory::Attributes + } + + DiagnosticCode::MissingPrimaryKey + | DiagnosticCode::InvalidIndexDefinition + | DiagnosticCode::DatabaseProviderMismatch + | DiagnosticCode::DeprecatedFeature + | DiagnosticCode::SecuritySuggestion => { + DiagnosticCategory::BusinessRules + } + + DiagnosticCode::PerformanceWarning + | DiagnosticCode::IndexSuggestion + | DiagnosticCode::QueryOptimizationHint => { + DiagnosticCategory::Performance + } + + DiagnosticCode::InvalidSchemaStructure + | DiagnosticCode::EmptyModel + | DiagnosticCode::ModelTooSmall + | DiagnosticCode::ModelTooLarge + | DiagnosticCode::UnusedDeclaration + | DiagnosticCode::MissingField + | DiagnosticCode::MissingDatasource => { + DiagnosticCategory::SchemaStructure + } + + DiagnosticCode::InternalError | DiagnosticCode::AnalysisTimeout => { + DiagnosticCategory::Internal + } + } + } + + /// Get the default severity for this diagnostic code. + #[must_use] + pub fn default_severity(self) -> DiagnosticSeverity { + match self { + DiagnosticCode::DeprecatedFeature + | DiagnosticCode::PerformanceWarning + | DiagnosticCode::IndexSuggestion + | DiagnosticCode::QueryOptimizationHint + | DiagnosticCode::EmptyModel + | DiagnosticCode::ModelTooSmall + | DiagnosticCode::ModelTooLarge + | DiagnosticCode::MissingDatasource + | DiagnosticCode::UnusedDeclaration => DiagnosticSeverity::Warning, + DiagnosticCode::SecuritySuggestion => DiagnosticSeverity::Info, + _ => DiagnosticSeverity::Error, + } + } +} + +/// Categories of diagnostic codes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiagnosticCategory { + SymbolResolution, + TypeSystem, + Relationships, + Attributes, + BusinessRules, + Performance, + SchemaStructure, + Internal, +} + +/// Fix hint with specific text replacements. +#[derive(Debug, Clone)] +pub struct FixHint { + /// Description of what this fix does + pub description: String, + + /// Specific text replacements to apply + pub replacements: Vec, + + /// Whether this fix is safe to apply automatically + pub is_safe_auto_fix: bool, +} + +impl FixHint { + /// Create a new fix hint. + #[must_use] + pub fn new(description: String) -> Self { + Self { + description, + replacements: Vec::new(), + is_safe_auto_fix: false, + } + } + + /// Add a text replacement to this fix hint. + #[must_use] + pub fn with_replacement(mut self, replacement: TextReplacement) -> Self { + self.replacements.push(replacement); + self + } + + /// Mark this fix as safe for automatic application. + #[must_use] + pub fn as_safe_auto_fix(mut self) -> Self { + self.is_safe_auto_fix = true; + self + } +} + +/// A specific text replacement for a fix hint. +#[derive(Debug, Clone)] +pub struct TextReplacement { + /// Span to replace + pub span: SymbolSpan, + + /// New text to insert + pub new_text: String, +} + +/// Related span that provides additional context to a diagnostic. +#[derive(Debug, Clone)] +pub struct RelatedSpan { + /// The related span + pub span: SymbolSpan, + + /// Message explaining the relevance of this span + pub message: String, +} + +/// Additional metadata for diagnostics. +#[derive(Debug, Clone)] +pub struct DiagnosticMetadata { + /// Key-value pairs of additional information + data: HashMap, +} + +impl DiagnosticMetadata { + #[must_use] + pub fn new() -> Self { + Self { + data: HashMap::new(), + } + } + + pub fn add(&mut self, key: String, value: String) { + self.data.insert(key, value); + } + + #[must_use] + pub fn get(&self, key: &str) -> Option<&str> { + self.data.get(key).map(std::string::String::as_str) + } + + #[must_use] + pub fn contains_key(&self, key: &str) -> bool { + self.data.contains_key(key) + } + + pub fn keys(&self) -> impl Iterator { + self.data.keys() + } + + pub fn values(&self) -> impl Iterator { + self.data.values() + } + + pub fn iter(&self) -> impl Iterator { + self.data.iter() + } +} + +impl Default for DiagnosticMetadata { + fn default() -> Self { + Self::new() + } +} + +/// Collector for diagnostics from multiple sources. +#[derive(Debug, Clone)] +pub struct DiagnosticCollector { + diagnostics: Vec, + max_diagnostics: Option, +} + +impl DiagnosticCollector { + /// Create a new diagnostic collector. + #[must_use] + pub fn new() -> Self { + Self { + diagnostics: Vec::new(), + max_diagnostics: None, + } + } + + /// Create a new diagnostic collector with a maximum capacity. + #[must_use] + pub fn with_limit(max_diagnostics: usize) -> Self { + Self { + diagnostics: Vec::new(), + max_diagnostics: Some(max_diagnostics), + } + } + + /// Add a diagnostic to the collection. + pub fn add(&mut self, diagnostic: SemanticDiagnostic) { + if let Some(max) = self.max_diagnostics + && self.diagnostics.len() >= max + { + return; + } + self.diagnostics.push(diagnostic); + } + + /// Add multiple diagnostics to the collection. + pub fn extend(&mut self, diagnostics: Vec) { + for diagnostic in diagnostics { + self.add(diagnostic); + } + } + + /// Get all diagnostics. + #[must_use] + pub fn all(&self) -> &[SemanticDiagnostic] { + &self.diagnostics + } + + /// Take all diagnostics, consuming the collector. + #[must_use] + pub fn take_all(self) -> Vec { + self.diagnostics + } + + /// Clear all diagnostics. + pub fn clear(&mut self) { + self.diagnostics.clear(); + } + + /// Get the number of diagnostics. + #[must_use] + pub fn len(&self) -> usize { + self.diagnostics.len() + } + + /// Check if the collector is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.diagnostics.is_empty() + } + + /// Check if the collector has reached its limit. + #[must_use] + pub fn is_at_limit(&self) -> bool { + self.max_diagnostics + .is_some_and(|max| self.diagnostics.len() >= max) + } + + /// Get diagnostics by severity. + #[must_use] + pub fn by_severity( + &self, + severity: DiagnosticSeverity, + ) -> Vec<&SemanticDiagnostic> { + self.diagnostics + .iter() + .filter(|d| d.severity == severity) + .collect() + } + + /// Get diagnostics by category. + #[must_use] + pub fn by_category( + &self, + category: DiagnosticCategory, + ) -> Vec<&SemanticDiagnostic> { + self.diagnostics + .iter() + .filter(|d| d.diagnostic_code.category() == category) + .collect() + } + + /// Get diagnostic counts by severity. + #[must_use] + pub fn severity_counts(&self) -> DiagnosticCounts { + let mut counts = DiagnosticCounts::default(); + for diagnostic in &self.diagnostics { + match diagnostic.severity { + DiagnosticSeverity::Error => counts.errors += 1, + DiagnosticSeverity::Warning => counts.warnings += 1, + DiagnosticSeverity::Info => counts.infos += 1, + } + } + counts + } +} + +impl Default for DiagnosticCollector { + fn default() -> Self { + Self::new() + } +} + +/// Counts of diagnostics by severity. +#[derive(Debug, Clone, Default)] +pub struct DiagnosticCounts { + pub errors: usize, + pub warnings: usize, + pub infos: usize, +} + +impl DiagnosticCounts { + #[must_use] + pub fn total(&self) -> usize { + self.errors + self.warnings + self.infos + } + + #[must_use] + pub fn has_errors(&self) -> bool { + self.errors > 0 + } + + #[must_use] + pub fn has_warnings(&self) -> bool { + self.warnings > 0 + } + + #[must_use] + pub fn has_infos(&self) -> bool { + self.infos > 0 + } +} + +/// Common diagnostic factory methods. +impl SemanticDiagnostic { + /// Create an "undeclared identifier" error. + #[must_use] + pub fn undeclared_identifier(span: SymbolSpan, name: &str) -> Self { + Self::error( + span, + format!("Identifier '{name}' is not declared"), + DiagnosticCode::UndeclaredIdentifier, + ) + .with_suggestion(format!( + "Check if '{name}' is spelled correctly and declared" + )) + } + + /// Create a "duplicate declaration" error. + #[must_use] + pub fn duplicate_declaration( + span: SymbolSpan, + name: &str, + existing_span: SymbolSpan, + ) -> Self { + Self::error( + span, + format!("Duplicate declaration of '{name}'"), + DiagnosticCode::DuplicateDeclaration, + ) + .with_suggestion(format!( + "Consider renaming one of the '{name}' declarations" + )) + .with_related_span(existing_span, "First declaration here".to_string()) + } + + /// Create an "unknown type" error. + #[must_use] + pub fn unknown_type(span: SymbolSpan, type_name: &str) -> Self { + Self::error( + span, + format!("Unknown type '{type_name}'"), + DiagnosticCode::UnknownType, + ) + .with_suggestion( + "Check if the type name is spelled correctly and declared" + .to_string(), + ) + } + + /// Create a "circular dependency" error. + #[must_use] + pub fn circular_dependency(span: SymbolSpan, cycle: &[String]) -> Self { + Self::error( + span, + format!("Circular dependency detected: {}", cycle.join(" -> ")), + DiagnosticCode::CircularDependency, + ) + .with_suggestion( + "Break the cycle by using optional fields or forward declarations" + .to_string(), + ) + } + + /// Create a "missing primary key" error. + #[must_use] + pub fn missing_primary_key(span: SymbolSpan, model_name: &str) -> Self { + let span_clone = span.clone(); + Self::error( + span, + format!("Model '{model_name}' must have a primary key"), + DiagnosticCode::MissingPrimaryKey, + ).with_suggestion("Add an @id attribute to one of the fields".to_string()) + .with_fix_hint(FixHint::new("Add id field".to_string()) + .with_replacement(TextReplacement { + span: span_clone, + new_text: format!("model {model_name} {{\n id String @id\n // other fields...\n}}"), + })) + } + + /// Create a "performance warning" diagnostic. + #[must_use] + pub fn performance_warning( + span: SymbolSpan, + message: String, + suggestion: String, + ) -> Self { + Self::warning(span, message, DiagnosticCode::PerformanceWarning) + .with_suggestion(suggestion) + } + + /// Create a "deprecated feature" warning. + #[must_use] + pub fn deprecated_feature( + span: SymbolSpan, + feature: &str, + replacement: Option<&str>, + ) -> Self { + let message = format!("Feature '{feature}' is deprecated"); + let suggestion = if let Some(replacement) = replacement { + format!("Use '{replacement}' instead") + } else { + "Consider removing this deprecated feature".to_string() + }; + + Self::warning(span, message, DiagnosticCode::DeprecatedFeature) + .with_suggestion(suggestion) + } +} diff --git a/src/core/semantic_analyzer/mod.rs b/src/core/semantic_analyzer/mod.rs new file mode 100644 index 0000000..a5ae27a --- /dev/null +++ b/src/core/semantic_analyzer/mod.rs @@ -0,0 +1,99 @@ +use compiler_macros::EnumKindName; + +pub mod context; +pub mod diagnostics; +pub mod symbol_table; +pub mod traits; +pub mod type_resolver; + +// Re-export main types for convenience +pub use context::{AnalysisContext, AnalysisResult}; +pub use diagnostics::{DiagnosticCode, FixHint, SemanticDiagnostic}; +pub use symbol_table::{Symbol, SymbolTable, SymbolType}; +pub use traits::{ + AttributeAnalyzer, DeclarationAnalyzer, PhaseAnalyzer, RelationshipAnalyzer, +}; + +/// Configuration options for semantic analysis. +/// Validation mode configuration +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValidationMode { + /// Lenient validation - allows warnings and continues + Lenient, + /// Strict validation - stops on first error + Strict, +} + +/// Feature validation configuration +#[derive(Debug, Clone)] +pub struct FeatureOptions { + /// Validate experimental features + pub validate_experimental: bool, + /// Enable performance warnings + pub performance_warnings: bool, + /// Enable parallel analysis where possible + pub enable_parallelism: bool, +} + +/// Concurrency mode for semantic analysis +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConcurrencyMode { + /// Single-threaded analysis + Sequential, + /// Multi-threaded analysis with specified thread count and threshold + Concurrent { + max_threads: usize, + threshold: usize, + }, +} + +#[derive(Debug, Clone)] +pub struct AnalyzerOptions { + /// Validation mode + pub validation_mode: ValidationMode, + + /// Feature validation options + pub features: FeatureOptions, + + /// Concurrency mode for analysis + pub concurrency: ConcurrencyMode, + + /// Maximum analysis time per phase + pub phase_timeout: std::time::Duration, + + /// Target database provider for validation + pub target_provider: Option, + + /// Maximum number of diagnostics to collect before stopping + pub max_diagnostics: usize, +} + +/// Supported database providers for provider-specific validation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumKindName)] +pub enum DatabaseProvider { + PostgreSQL, + MySQL, + SQLite, + SQLServer, + MongoDB, +} + +impl Default for AnalyzerOptions { + fn default() -> Self { + Self { + validation_mode: ValidationMode::Lenient, + features: FeatureOptions { + validate_experimental: true, + performance_warnings: true, + enable_parallelism: true, + }, + concurrency: ConcurrencyMode::Concurrent { + max_threads: 4, + threshold: 2, + }, + phase_timeout: std::time::Duration::from_secs(30), + target_provider: None, + max_diagnostics: 100, + } + } +} diff --git a/src/core/semantic_analyzer/symbol_table.rs b/src/core/semantic_analyzer/symbol_table.rs new file mode 100644 index 0000000..ad260f6 --- /dev/null +++ b/src/core/semantic_analyzer/symbol_table.rs @@ -0,0 +1,624 @@ +//! Symbol table for tracking schema declarations and their metadata. +//! +//! The symbol table maintains a hierarchical view of all declared symbols in +//! the schema, including models, enums, fields, and their relationships. + +use crate::core::parser::ast::{ + DatasourceDecl, EnumDecl, EnumValue, FieldDecl, GeneratorDecl, ModelDecl, +}; +use crate::core::scanner::tokens::SymbolSpan; +use std::collections::HashMap; + +/// Global symbol table for schema analysis. +/// +/// The symbol table tracks all declared identifiers and their metadata +/// across different scopes (global, model-local, etc.). +#[derive(Debug, Clone)] +pub struct SymbolTable { + /// Global scope symbols (models, enums, datasources, generators, types) + global_symbols: HashMap, + + /// Model-scoped symbols (fields within each model) + model_scopes: HashMap>, + + /// Enum-scoped symbols (values within each enum) + enum_scopes: HashMap>, + + /// Built-in types and functions + builtin_symbols: HashMap, +} + +impl SymbolTable { + /// Create a new symbol table with built-in symbols. + #[must_use] + pub fn new() -> Self { + let mut table = Self { + global_symbols: HashMap::new(), + model_scopes: HashMap::new(), + enum_scopes: HashMap::new(), + builtin_symbols: HashMap::new(), + }; + + table.initialize_builtins(); + table + } + + /// Add a model symbol to the global scope. + /// + /// # Errors + /// + /// Returns a `SymbolError` if the model name conflicts with an existing symbol. + pub fn add_model(&mut self, model: &ModelDecl) -> Result<(), SymbolError> { + let symbol = Symbol::new_model(model); + self.add_global_symbol(symbol)?; + + // Create scope for model fields + let mut field_scope = HashMap::new(); + + // Add field symbols + for member in &model.members { + if let crate::core::parser::ast::ModelMember::Field(field) = member + { + let field_symbol = Symbol::new_field(field, &model.name.text); + if let Some(existing) = + field_scope.insert(field.name.text.clone(), field_symbol) + { + return Err(SymbolError::DuplicateSymbol { + name: field.name.text.clone(), + scope: model.name.text.clone(), + existing_span: existing.declaration_span, + new_span: field.span.clone(), + }); + } + } + } + + self.model_scopes + .insert(model.name.text.clone(), field_scope); + Ok(()) + } + + /// Add an enum symbol to the global scope. + /// + /// # Errors + /// + /// Returns a `SymbolError` if the enum name conflicts with an existing symbol. + pub fn add_enum( + &mut self, + enum_decl: &EnumDecl, + ) -> Result<(), SymbolError> { + let symbol = Symbol::new_enum(enum_decl); + self.add_global_symbol(symbol)?; + + // Create scope for enum values + let mut value_scope = HashMap::new(); + + // Add enum value symbols + for member in &enum_decl.members { + if let crate::core::parser::ast::EnumMember::Value(value) = member { + let value_symbol = + Symbol::new_enum_value(value, &enum_decl.name.text); + if let Some(existing) = + value_scope.insert(value.name.text.clone(), value_symbol) + { + return Err(SymbolError::DuplicateSymbol { + name: value.name.text.clone(), + scope: enum_decl.name.text.clone(), + existing_span: existing.declaration_span, + new_span: value.span.clone(), + }); + } + } + } + + self.enum_scopes + .insert(enum_decl.name.text.clone(), value_scope); + Ok(()) + } + + /// Add a datasource symbol to the global scope. + /// + /// # Errors + /// + /// Returns a `SymbolError` if the datasource name conflicts with an existing symbol. + pub fn add_datasource( + &mut self, + datasource: &DatasourceDecl, + ) -> Result<(), SymbolError> { + let symbol = Symbol::new_datasource(datasource); + self.add_global_symbol(symbol) + } + + /// Add a generator symbol to the global scope. + /// + /// # Errors + /// + /// Returns a `SymbolError` if the generator name conflicts with an existing symbol. + pub fn add_generator( + &mut self, + generator: &GeneratorDecl, + ) -> Result<(), SymbolError> { + let symbol = Symbol::new_generator(generator); + self.add_global_symbol(symbol) + } + + /// Look up a symbol in the global scope. + #[must_use] + pub fn lookup_global(&self, name: &str) -> Option<&Symbol> { + self.global_symbols + .get(name) + .or_else(|| self.builtin_symbols.get(name)) + } + + /// Look up a field symbol within a model scope. + #[must_use] + pub fn lookup_field( + &self, + model_name: &str, + field_name: &str, + ) -> Option<&Symbol> { + self.model_scopes.get(model_name)?.get(field_name) + } + + /// Look up an enum value within an enum scope. + #[must_use] + pub fn lookup_enum_value( + &self, + enum_name: &str, + value_name: &str, + ) -> Option<&Symbol> { + self.enum_scopes.get(enum_name)?.get(value_name) + } + + /// Get all models in the symbol table. + pub fn models(&self) -> impl Iterator { + self.global_symbols.iter().filter(|(_, symbol)| { + matches!(symbol.symbol_type, SymbolType::Model(_)) + }) + } + + /// Get all enums in the symbol table. + pub fn enums(&self) -> impl Iterator { + self.global_symbols.iter().filter(|(_, symbol)| { + matches!(symbol.symbol_type, SymbolType::Enum(_)) + }) + } + + /// Get all fields in a specific model. + #[must_use] + pub fn model_fields( + &self, + model_name: &str, + ) -> Option> { + self.model_scopes.get(model_name).map(|scope| scope.iter()) + } + + /// Get all values in a specific enum. + #[must_use] + pub fn enum_values( + &self, + enum_name: &str, + ) -> Option> { + self.enum_scopes.get(enum_name).map(|scope| scope.iter()) + } + + /// Check if a symbol exists in any scope. + #[must_use] + pub fn contains(&self, name: &str) -> bool { + self.lookup_global(name).is_some() + } + + /// Get the total number of symbols across all scopes. + #[must_use] + pub fn total_symbol_count(&self) -> usize { + self.global_symbols.len() + + self + .model_scopes + .values() + .map(std::collections::HashMap::len) + .sum::() + + self + .enum_scopes + .values() + .map(std::collections::HashMap::len) + .sum::() + } + + /// Add a symbol to the global scope with duplicate checking. + fn add_global_symbol(&mut self, symbol: Symbol) -> Result<(), SymbolError> { + let name = symbol.name.clone(); + + if let Some(existing) = self.global_symbols.get(&name) { + return Err(SymbolError::DuplicateSymbol { + name, + scope: "global".to_string(), + existing_span: existing.declaration_span.clone(), + new_span: symbol.declaration_span.clone(), + }); + } + + self.global_symbols.insert(name, symbol); + Ok(()) + } + + /// Initialize built-in symbols (scalar types, functions, etc.). + fn initialize_builtins(&mut self) { + // Built-in scalar types + for scalar_type in &[ + "String", "Int", "Float", "Boolean", "DateTime", "Json", "Bytes", + "Decimal", "BigInt", "Uuid", + ] { + let symbol = Symbol::new_builtin_type(scalar_type); + self.builtin_symbols + .insert((*scalar_type).to_string(), symbol); + } + + // Built-in functions (for use in default values, etc.) + for function in &["now", "autoincrement", "cuid", "uuid"] { + let symbol = Symbol::new_builtin_function(function); + self.builtin_symbols.insert((*function).to_string(), symbol); + } + } +} + +impl Default for SymbolTable { + fn default() -> Self { + Self::new() + } +} + +/// A symbol in the symbol table. +#[derive(Debug, Clone)] +pub struct Symbol { + /// The name of the symbol + pub name: String, + + /// The type and metadata of the symbol + pub symbol_type: SymbolType, + + /// The span where this symbol was declared + pub declaration_span: SymbolSpan, + + /// Visibility of this symbol + pub visibility: Visibility, + + /// Additional metadata about the symbol + pub metadata: SymbolMetadata, +} + +impl Symbol { + /// Create a new model symbol. + #[must_use] + pub fn new_model(model: &ModelDecl) -> Self { + Self { + name: model.name.text.clone(), + symbol_type: SymbolType::Model(ModelSymbol { + field_count: model + .members + .iter() + .filter(|m| { + matches!( + m, + crate::core::parser::ast::ModelMember::Field(_) + ) + }) + .count(), + has_primary_key: model.members.iter().any(|m| { + if let crate::core::parser::ast::ModelMember::Field(field) = + m + { + field.attrs.iter().any(|attr| { + attr.name.parts.len() == 1 + && attr.name.parts[0].text == "id" + }) + } else { + false + } + }), + block_attributes: model.attrs.len(), + }), + declaration_span: model.span.clone(), + visibility: Visibility::Public, + metadata: SymbolMetadata::new(), + } + } + + /// Create a new enum symbol. + #[must_use] + pub fn new_enum(enum_decl: &EnumDecl) -> Self { + Self { + name: enum_decl.name.text.clone(), + symbol_type: SymbolType::Enum(EnumSymbol { + value_count: enum_decl + .members + .iter() + .filter(|m| { + matches!( + m, + crate::core::parser::ast::EnumMember::Value(_) + ) + }) + .count(), + block_attributes: enum_decl.attrs.len(), + }), + declaration_span: enum_decl.span.clone(), + visibility: Visibility::Public, + metadata: SymbolMetadata::new(), + } + } + + /// Create a new field symbol. + #[must_use] + pub fn new_field(field: &FieldDecl, parent_model: &str) -> Self { + Self { + name: field.name.text.clone(), + symbol_type: SymbolType::Field(FieldSymbol { + parent_model: parent_model.to_string(), + is_optional: field.optional, + is_list: matches!( + &field.r#type, + crate::core::parser::ast::TypeRef::List(_) + ), + attribute_count: field.attrs.len(), + }), + declaration_span: field.span.clone(), + visibility: Visibility::Public, + metadata: SymbolMetadata::new(), + } + } + + /// Create a new enum value symbol. + #[must_use] + pub fn new_enum_value(value: &EnumValue, parent_enum: &str) -> Self { + Self { + name: value.name.text.clone(), + symbol_type: SymbolType::EnumValue(EnumValueSymbol { + parent_enum: parent_enum.to_string(), + attribute_count: value.attrs.len(), + }), + declaration_span: value.span.clone(), + visibility: Visibility::Public, + metadata: SymbolMetadata::new(), + } + } + + /// Create a new datasource symbol. + #[must_use] + pub fn new_datasource(datasource: &DatasourceDecl) -> Self { + Self { + name: datasource.name.text.clone(), + symbol_type: SymbolType::Datasource(DatasourceSymbol { + assignment_count: datasource.assignments.len(), + }), + declaration_span: datasource.span.clone(), + visibility: Visibility::Public, + metadata: SymbolMetadata::new(), + } + } + + /// Create a new generator symbol. + #[must_use] + pub fn new_generator(generator: &GeneratorDecl) -> Self { + Self { + name: generator.name.text.clone(), + symbol_type: SymbolType::Generator(GeneratorSymbol { + assignment_count: generator.assignments.len(), + }), + declaration_span: generator.span.clone(), + visibility: Visibility::Public, + metadata: SymbolMetadata::new(), + } + } + + /// Create a built-in type symbol. + #[must_use] + pub fn new_builtin_type(name: &str) -> Self { + Self { + name: name.to_string(), + symbol_type: SymbolType::BuiltinType(BuiltinTypeSymbol { + type_category: TypeCategory::Scalar, + }), + declaration_span: SymbolSpan { + start: crate::core::scanner::tokens::SymbolLocation { + line: 0, + column: 0, + }, + end: crate::core::scanner::tokens::SymbolLocation { + line: 0, + column: 0, + }, + }, + visibility: Visibility::Public, + metadata: SymbolMetadata::builtin(), + } + } + + /// Create a built-in function symbol. + #[must_use] + pub fn new_builtin_function(name: &str) -> Self { + Self { + name: name.to_string(), + symbol_type: SymbolType::BuiltinFunction(BuiltinFunctionSymbol { + function_category: FunctionCategory::Generator, + }), + declaration_span: SymbolSpan { + start: crate::core::scanner::tokens::SymbolLocation { + line: 0, + column: 0, + }, + end: crate::core::scanner::tokens::SymbolLocation { + line: 0, + column: 0, + }, + }, + visibility: Visibility::Public, + metadata: SymbolMetadata::builtin(), + } + } +} + +/// Types of symbols that can be stored in the symbol table. +#[derive(Debug, Clone)] +pub enum SymbolType { + Model(ModelSymbol), + Enum(EnumSymbol), + Field(FieldSymbol), + EnumValue(EnumValueSymbol), + Datasource(DatasourceSymbol), + Generator(GeneratorSymbol), + TypeAlias(TypeAliasSymbol), + BuiltinType(BuiltinTypeSymbol), + BuiltinFunction(BuiltinFunctionSymbol), +} + +/// Symbol metadata for model declarations. +#[derive(Debug, Clone)] +pub struct ModelSymbol { + pub field_count: usize, + pub has_primary_key: bool, + pub block_attributes: usize, +} + +/// Symbol metadata for enum declarations. +#[derive(Debug, Clone)] +pub struct EnumSymbol { + pub value_count: usize, + pub block_attributes: usize, +} + +/// Symbol metadata for field declarations. +#[derive(Debug, Clone)] +pub struct FieldSymbol { + pub parent_model: String, + pub is_optional: bool, + pub is_list: bool, + pub attribute_count: usize, +} + +/// Symbol metadata for enum value declarations. +#[derive(Debug, Clone)] +pub struct EnumValueSymbol { + pub parent_enum: String, + pub attribute_count: usize, +} + +/// Symbol metadata for datasource declarations. +#[derive(Debug, Clone)] +pub struct DatasourceSymbol { + pub assignment_count: usize, +} + +/// Symbol metadata for generator declarations. +#[derive(Debug, Clone)] +pub struct GeneratorSymbol { + pub assignment_count: usize, +} + +/// Symbol metadata for type alias declarations. +#[derive(Debug, Clone)] +pub struct TypeAliasSymbol { + pub target_type: String, // Will be properly typed later +} + +/// Symbol metadata for built-in type symbols. +#[derive(Debug, Clone)] +pub struct BuiltinTypeSymbol { + pub type_category: TypeCategory, +} + +/// Symbol metadata for built-in function symbols. +#[derive(Debug, Clone)] +pub struct BuiltinFunctionSymbol { + pub function_category: FunctionCategory, +} + +/// Categories of built-in types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeCategory { + Scalar, + Composite, +} + +/// Categories of built-in functions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FunctionCategory { + Generator, + Transformer, +} + +/// Symbol visibility levels. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Visibility { + Public, + Private, +} + +/// Additional metadata about symbols. +#[derive(Debug, Clone)] +pub struct SymbolMetadata { + pub is_builtin: bool, + pub documentation: Option, + pub tags: Vec, +} + +impl Default for SymbolMetadata { + fn default() -> Self { + Self::new() + } +} + +impl SymbolMetadata { + #[must_use] + pub fn new() -> Self { + Self { + is_builtin: false, + documentation: None, + tags: Vec::new(), + } + } + + #[must_use] + pub fn builtin() -> Self { + Self { + is_builtin: true, + documentation: None, + tags: Vec::new(), + } + } +} + +/// Errors that can occur during symbol table operations. +#[derive(Debug, Clone)] +pub enum SymbolError { + /// Duplicate symbol declaration + DuplicateSymbol { + name: String, + scope: String, + existing_span: SymbolSpan, + new_span: SymbolSpan, + }, + + /// Symbol not found + SymbolNotFound { name: String, scope: String }, + + /// Invalid symbol operation + InvalidOperation { message: String }, +} + +impl std::fmt::Display for SymbolError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SymbolError::DuplicateSymbol { name, scope, .. } => { + write!(f, "Duplicate symbol '{name}' in scope '{scope}'") + } + SymbolError::SymbolNotFound { name, scope } => { + write!(f, "Symbol '{name}' not found in scope '{scope}'") + } + SymbolError::InvalidOperation { message } => { + write!(f, "Invalid symbol operation: {message}") + } + } + } +} + +impl std::error::Error for SymbolError {} diff --git a/src/core/semantic_analyzer/traits.rs b/src/core/semantic_analyzer/traits.rs new file mode 100644 index 0000000..0898393 --- /dev/null +++ b/src/core/semantic_analyzer/traits.rs @@ -0,0 +1,239 @@ +//! Core traits for semantic analysis components. +//! +//! This module defines the trait-based architecture that makes the semantic +//! analyzer fully pluggable. Different analyzer implementations can be composed +//! together to create custom analysis pipelines. + +use crate::core::parser::ast::{FieldAttribute, Schema}; +use crate::core::semantic_analyzer::{ + context::{AnalysisContext, PhaseResult}, + diagnostics::SemanticDiagnostic, +}; + +/// Core trait for analysis phases. +/// +/// Each semantic analysis phase implements this trait to provide a standardized +/// interface for the orchestrator. Phases can declare dependencies and specify +/// whether they support parallel execution. +/// +/// ## Examples +/// +/// ```rust,ignore +/// struct MyAnalyzer; +/// +/// impl PhaseAnalyzer for MyAnalyzer { +/// fn phase_name(&self) -> &'static str { +/// "Custom Analysis" +/// } +/// +/// fn analyze(&self, schema: &Schema, context: &AnalysisContext) -> PhaseResult { +/// let mut diagnostics = Vec::new(); +/// // Perform analysis... +/// PhaseResult::new(diagnostics) +/// } +/// } +/// ``` +pub trait PhaseAnalyzer: Send + Sync { + /// Get the name of this analysis phase for debugging and dependency resolution. + fn phase_name(&self) -> &'static str; + + /// Analyze the schema in this phase. + /// + /// The analyzer should examine the schema and context, perform its specific + /// analysis, and return diagnostics. It may also update the shared state + /// through the context's Arc<`RwLock`<>> protected fields. + fn analyze( + &self, + schema: &Schema, + context: &AnalysisContext, + ) -> PhaseResult; + + /// Get the analysis dependencies (phases that must run before this one). + /// + /// Dependencies are specified by phase name. The orchestrator will ensure + /// that all dependencies are completed before running this analyzer. + fn dependencies(&self) -> &[&'static str] { + &[] + } + + /// Whether this analyzer can run in parallel with others. + /// + /// Analyzers that only read from the context and don't modify shared state + /// can often run in parallel with other analyzers that have the same + /// property. + fn supports_parallel_execution(&self) -> bool { + false + } + + /// Get the priority of this analyzer within its dependency level. + /// + /// When multiple analyzers can run at the same time, those with higher + /// priority will be scheduled first. Default priority is 0. + fn priority(&self) -> i32 { + 0 + } +} + +/// Trait for analyzing individual declarations. +/// +/// This trait allows for fine-grained analysis of specific declaration types +/// (models, enums, etc.). Analyzers can implement this trait to focus on +/// particular aspects of declarations. +pub trait DeclarationAnalyzer: Send + Sync { + /// Analyze a specific declaration and return any diagnostics. + fn analyze_declaration( + &self, + decl: &T, + context: &AnalysisContext, + ) -> Vec; + + /// Get the name of this declaration analyzer for debugging. + fn analyzer_name(&self) -> &'static str; +} + +/// Trait for analyzing relationships between declarations. +/// +/// Relationship analyzers examine how different schema elements interact, +/// such as foreign key relationships, inheritance, or dependencies. +pub trait RelationshipAnalyzer: Send + Sync { + /// Analyze relationships within the entire schema. + fn analyze_relationships( + &self, + schema: &Schema, + context: &AnalysisContext, + ) -> Vec; + + /// Get the name of this relationship analyzer. + fn analyzer_name(&self) -> &'static str; +} + +/// Trait for analyzing attributes and their arguments. +/// +/// Attribute analyzers validate that attributes are used correctly, have +/// valid arguments, and don't conflict with other attributes. +pub trait AttributeAnalyzer: Send + Sync { + /// Analyze a specific attribute in context. + fn analyze_attribute( + &self, + attr: &FieldAttribute, + context: &AttributeContext, + ) -> Vec; + + /// Get the set of attribute names this analyzer handles. + fn supported_attributes(&self) -> &[&'static str]; + + /// Get the name of this attribute analyzer. + fn analyzer_name(&self) -> &'static str; +} + +/// Context for attribute analysis. +/// +/// Provides additional context about where an attribute appears and what +/// element it's attached to. +#[derive(Debug, Clone)] +pub struct AttributeContext<'a> { + /// The schema element this attribute is attached to + pub element_type: AttributeElementType, + + /// The name of the parent element (model name, field name, etc.) + pub element_name: &'a str, + + /// Reference to the main analysis context + pub analysis_context: &'a AnalysisContext, +} + +/// Types of schema elements that can have attributes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AttributeElementType { + Model, + Field, + Enum, + EnumValue, + Datasource, + Generator, +} + +/// Trait for custom validation rules. +/// +/// This trait allows users to implement custom business logic and validation +/// rules that are specific to their use case. +pub trait CustomRule: Send + Sync { + /// Apply the custom rule to the schema. + fn apply_rule( + &self, + schema: &Schema, + context: &AnalysisContext, + ) -> Vec; + + /// Get the name of this custom rule. + fn rule_name(&self) -> &'static str; + + /// Get the priority of this rule (higher priority rules run first). + fn priority(&self) -> i32 { + 0 + } +} + +/// Trait for composable analyzer components. +/// +/// This trait allows analyzers to be composed together into larger analysis +/// units while maintaining introspection capabilities for debugging and testing. +pub trait CompositeAnalyzer { + /// Get the name of this analyzer component. + fn component_name(&self) -> &'static str; + + /// Get child analyzer components if this is a composite. + fn child_components(&self) -> Vec<&dyn CompositeAnalyzer> { + Vec::new() + } + + /// Get a hierarchical description of this analyzer's structure. + fn structure_description(&self) -> String { + let mut desc = self.component_name().to_string(); + let children = self.child_components(); + if !children.is_empty() { + desc.push_str(" { "); + for (i, child) in children.iter().enumerate() { + if i > 0 { + desc.push_str(", "); + } + desc.push_str(&child.structure_description()); + } + desc.push_str(" }"); + } + desc + } +} + +/// Trait for analyzers that can provide progress information. +/// +/// This trait enables progress monitoring and timeout detection for long-running +/// analysis operations. +pub trait ProgressReporting { + /// Get the current progress as a percentage (0.0 to 1.0). + fn progress(&self) -> f32; + + /// Get a description of what the analyzer is currently doing. + fn current_activity(&self) -> Option<&str>; + + /// Check if the analyzer is still making progress. + fn is_making_progress(&self) -> bool; + + /// Get the time when this analyzer last made progress. + fn last_progress_time(&self) -> std::time::Instant; +} + +/// Trait for analyzers that support cancellation. +/// +/// Analyzers implementing this trait can be interrupted and stopped gracefully +/// when timeouts occur or when the user requests cancellation. +pub trait Cancellable { + /// Request that the analyzer cancel its current operation. + fn request_cancellation(&mut self); + + /// Check if cancellation has been requested. + fn is_cancelled(&self) -> bool; + + /// Reset the cancellation state. + fn reset_cancellation(&mut self); +} diff --git a/src/core/semantic_analyzer/type_resolver.rs b/src/core/semantic_analyzer/type_resolver.rs new file mode 100644 index 0000000..41cc501 --- /dev/null +++ b/src/core/semantic_analyzer/type_resolver.rs @@ -0,0 +1,601 @@ +//! Type resolution and validation system. +//! +//! The type resolver handles resolving type references in the schema, +//! building a complete type system, and detecting type-related errors +//! such as circular dependencies and unknown types. + +use crate::core::parser::ast::{NamedType, TypeRef}; +use crate::core::semantic_analyzer::symbol_table::{SymbolTable, SymbolType}; +use std::collections::{HashMap, HashSet}; + +/// Type resolver for schema analysis. +/// +/// The type resolver maintains a cache of resolved types and handles +/// complex type resolution including generic types, constraints, +/// and dependency analysis. +#[derive(Debug, Clone)] +pub struct TypeResolver { + /// Cache of resolved types + resolved_types: HashMap, + + /// Type dependency graph for circular dependency detection + type_dependencies: HashMap>, + + /// Type constraints and validation rules + type_constraints: HashMap, +} + +impl TypeResolver { + /// Create a new type resolver. + #[must_use] + pub fn new() -> Self { + Self { + resolved_types: HashMap::new(), + type_dependencies: HashMap::new(), + type_constraints: HashMap::new(), + } + } + + /// Resolve a type reference against the symbol table. + /// + /// # Errors + /// + /// Returns a `TypeResolutionError` if the type cannot be resolved. + pub fn resolve_type( + &mut self, + type_ref: &TypeRef, + symbol_table: &SymbolTable, + ) -> Result { + // Check cache first + if let Some(resolved) = self.resolved_types.get(type_ref) { + return Ok(resolved.clone()); + } + + let resolved = self.resolve_type_uncached(type_ref, symbol_table)?; + self.resolved_types + .insert(type_ref.clone(), resolved.clone()); + Ok(resolved) + } + + /// Resolve a type reference without using the cache. + fn resolve_type_uncached( + &mut self, + type_ref: &TypeRef, + symbol_table: &SymbolTable, + ) -> Result { + match type_ref { + TypeRef::Named(named_type) => { + Self::resolve_named_type(named_type, symbol_table) + } + TypeRef::List(list_type) => { + let element_type = + self.resolve_type(&list_type.inner, symbol_table)?; + Ok(ResolvedType::List(Box::new(element_type))) + } + } + } + + /// Resolve a named type reference. + fn resolve_named_type( + named_type: &NamedType, + symbol_table: &SymbolTable, + ) -> Result { + let type_name = &named_type.name.parts[0].text; + + // Look up the type in the symbol table + if let Some(symbol) = symbol_table.lookup_global(type_name) { + match &symbol.symbol_type { + SymbolType::Model(_) => { + Ok(ResolvedType::Model(type_name.clone())) + } + SymbolType::Enum(_) => { + Ok(ResolvedType::Enum(type_name.clone())) + } + SymbolType::BuiltinType(_) => { + // Map to appropriate scalar type + let scalar_type = Self::map_builtin_to_scalar(type_name)?; + Ok(ResolvedType::Scalar(scalar_type)) + } + SymbolType::TypeAlias(alias) => { + // For now, just resolve to the target type name + // Full alias resolution would require recursive resolution + Ok(ResolvedType::Alias { + name: type_name.clone(), + target: alias.target_type.clone(), + }) + } + _ => Err(TypeResolutionError::NotAType { + name: type_name.clone(), + actual_symbol_type: format!("{:?}", symbol.symbol_type), + }), + } + } else { + Err(TypeResolutionError::UnknownType { + name: type_name.clone(), + }) + } + } + + /// Map a built-in type name to a scalar type. + fn map_builtin_to_scalar( + type_name: &str, + ) -> Result { + match type_name { + "String" => Ok(ScalarType::String), + "Int" => Ok(ScalarType::Int), + "Float" => Ok(ScalarType::Float), + "Boolean" => Ok(ScalarType::Boolean), + "DateTime" => Ok(ScalarType::DateTime), + "Json" => Ok(ScalarType::Json), + "Bytes" => Ok(ScalarType::Bytes), + "Decimal" => Ok(ScalarType::Decimal), + "BigInt" => Ok(ScalarType::BigInt), + "Uuid" => Ok(ScalarType::Uuid), + _ => Err(TypeResolutionError::UnknownBuiltinType { + name: type_name.to_string(), + }), + } + } + + /// Add a type dependency edge for cycle detection. + pub fn add_type_dependency(&mut self, from: String, to: String) { + self.type_dependencies.entry(from).or_default().insert(to); + } + + /// Check for circular type dependencies. + /// + /// # Errors + /// + /// Returns a `TypeResolutionError` if circular dependencies are detected. + pub fn check_circular_dependencies( + &self, + ) -> Result<(), TypeResolutionError> { + for start_type in self.type_dependencies.keys() { + if self.has_circular_dependency(start_type, &mut HashSet::new())? { + let cycle = self.find_cycle(start_type); + return Err(TypeResolutionError::CircularDependency { cycle }); + } + } + Ok(()) + } + + /// Check if a type has a circular dependency. + fn has_circular_dependency( + &self, + type_name: &str, + visited: &mut HashSet, + ) -> Result { + if visited.contains(type_name) { + return Ok(true); + } + + visited.insert(type_name.to_string()); + + if let Some(dependencies) = self.type_dependencies.get(type_name) { + for dep in dependencies { + if self.has_circular_dependency(dep, visited)? { + return Ok(true); + } + } + } + + visited.remove(type_name); + Ok(false) + } + + /// Find the actual cycle path for error reporting. + fn find_cycle(&self, start_type: &str) -> Vec { + let mut path = Vec::new(); + let mut visited = HashSet::new(); + + if self.find_cycle_path(start_type, &mut path, &mut visited) { + path + } else { + // This shouldn't happen if has_circular_dependency returned true + vec![start_type.to_string()] + } + } + + /// Find the cycle path recursively. + fn find_cycle_path( + &self, + type_name: &str, + path: &mut Vec, + visited: &mut HashSet, + ) -> bool { + if path.contains(&type_name.to_string()) { + path.push(type_name.to_string()); + return true; + } + + if visited.contains(type_name) { + return false; + } + + visited.insert(type_name.to_string()); + path.push(type_name.to_string()); + + if let Some(dependencies) = self.type_dependencies.get(type_name) { + for dep in dependencies { + if self.find_cycle_path(dep, path, visited) { + return true; + } + } + } + + path.pop(); + false + } + + /// Get type constraints for a specific type. + #[must_use] + pub fn get_constraints(&self, type_name: &str) -> Option<&TypeConstraints> { + self.type_constraints.get(type_name) + } + + /// Set type constraints for a specific type. + pub fn set_constraints( + &mut self, + type_name: String, + constraints: TypeConstraints, + ) { + self.type_constraints.insert(type_name, constraints); + } + + /// Validate type compatibility between two types. + #[must_use] + pub fn are_compatible(from: &ResolvedType, to: &ResolvedType) -> bool { + match (from, to) { + // Same types are always compatible + (a, b) if a == b => true, + + // Optional can accept the inner type + (inner, ResolvedType::Optional(opt_inner)) => { + Self::are_compatible(inner, opt_inner) + } + + // List element compatibility + (ResolvedType::List(a), ResolvedType::List(b)) => { + Self::are_compatible(a, b) + } + + // Scalar type compatibility rules + ( + ResolvedType::Scalar(ScalarType::Int), + ResolvedType::Scalar(ScalarType::Float | ScalarType::BigInt), + ) => true, + + // No other implicit conversions allowed + _ => false, + } + } + + /// Clear the resolution cache. + pub fn clear_cache(&mut self) { + self.resolved_types.clear(); + } + + /// Get statistics about the type resolver. + #[must_use] + pub fn stats(&self) -> TypeResolverStats { + TypeResolverStats { + cached_types: self.resolved_types.len(), + dependency_edges: self + .type_dependencies + .values() + .map(std::collections::HashSet::len) + .sum(), + constraint_count: self.type_constraints.len(), + } + } +} + +impl Default for TypeResolver { + fn default() -> Self { + Self::new() + } +} + +/// A fully resolved type. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ResolvedType { + /// Scalar type (built-in primitive) + Scalar(ScalarType), + + /// Model reference + Model(String), + + /// Enum reference + Enum(String), + + /// List of another type + List(Box), + + /// Optional (nullable) type + Optional(Box), + + /// Type alias + Alias { name: String, target: String }, + + /// Composite type (for complex scenarios) + Composite(CompositeType), +} + +impl ResolvedType { + /// Check if this type is optional (nullable). + #[must_use] + pub fn is_optional(&self) -> bool { + matches!(self, ResolvedType::Optional(_)) + } + + /// Check if this type is a list. + #[must_use] + pub fn is_list(&self) -> bool { + matches!(self, ResolvedType::List(_)) + } + + /// Check if this type is a scalar. + #[must_use] + pub fn is_scalar(&self) -> bool { + matches!(self, ResolvedType::Scalar(_)) + } + + /// Get the underlying type, removing optional wrapper. + #[must_use] + pub fn unwrap_optional(&self) -> &ResolvedType { + match self { + ResolvedType::Optional(inner) => inner.unwrap_optional(), + other => other, + } + } + + /// Get a human-readable name for this type. + #[must_use] + pub fn display_name(&self) -> String { + match self { + ResolvedType::Scalar(scalar) => scalar.display_name(), + ResolvedType::Model(name) + | ResolvedType::Enum(name) + | ResolvedType::Alias { name, .. } => name.clone(), + ResolvedType::List(inner) => format!("{}[]", inner.display_name()), + ResolvedType::Optional(inner) => { + format!("{}?", inner.display_name()) + } + ResolvedType::Composite(comp) => comp.name.clone(), + } + } +} + +/// Built-in scalar types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ScalarType { + String, + Int, + Float, + Boolean, + DateTime, + Json, + Bytes, + Decimal, + BigInt, + Uuid, +} + +impl ScalarType { + /// Get the display name for this scalar type. + #[must_use] + pub fn display_name(self) -> String { + match self { + ScalarType::String => "String".to_string(), + ScalarType::Int => "Int".to_string(), + ScalarType::Float => "Float".to_string(), + ScalarType::Boolean => "Boolean".to_string(), + ScalarType::DateTime => "DateTime".to_string(), + ScalarType::Json => "Json".to_string(), + ScalarType::Bytes => "Bytes".to_string(), + ScalarType::Decimal => "Decimal".to_string(), + ScalarType::BigInt => "BigInt".to_string(), + ScalarType::Uuid => "Uuid".to_string(), + } + } + + /// Check if this scalar type supports null values by default. + #[must_use] + pub fn is_nullable_by_default(self) -> bool { + // In Prisma, most types are non-nullable by default + false + } + + /// Get the category of this scalar type. + #[must_use] + pub fn category(self) -> ScalarCategory { + match self { + ScalarType::String | ScalarType::Uuid => ScalarCategory::Text, + ScalarType::Int | ScalarType::BigInt => ScalarCategory::Integer, + ScalarType::Float | ScalarType::Decimal => ScalarCategory::Decimal, + ScalarType::Boolean => ScalarCategory::Boolean, + ScalarType::DateTime => ScalarCategory::DateTime, + ScalarType::Json => ScalarCategory::Json, + ScalarType::Bytes => ScalarCategory::Binary, + } + } +} + +/// Categories of scalar types for validation rules. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScalarCategory { + Text, + Integer, + Decimal, + Boolean, + DateTime, + Json, + Binary, +} + +/// Composite type for complex scenarios. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CompositeType { + pub name: String, + pub fields: Vec, +} + +/// Field in a composite type. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CompositeField { + pub name: String, + pub field_type: Box, + pub is_optional: bool, +} + +/// Type constraints for validation. +#[derive(Debug, Clone)] +pub struct TypeConstraints { + /// Whether the type can be null + pub nullable: bool, + + /// Whether the type can be used in lists + pub listable: bool, + + /// Valid attribute names for this type + pub valid_attributes: Vec, + + /// Database-specific constraints + pub database_constraints: HashMap, +} + +impl Default for TypeConstraints { + fn default() -> Self { + Self::new() + } +} + +impl TypeConstraints { + /// Create default constraints for a type. + #[must_use] + pub fn new() -> Self { + Self { + nullable: false, + listable: true, + valid_attributes: Vec::new(), + database_constraints: HashMap::new(), + } + } + + /// Create constraints for a scalar type. + #[must_use] + pub fn for_scalar(scalar: ScalarType) -> Self { + let mut constraints = Self::new(); + + // Add common scalar attributes + constraints.valid_attributes.extend([ + "id".to_string(), + "unique".to_string(), + "default".to_string(), + ]); + + // Add type-specific attributes + match scalar { + ScalarType::String => { + constraints.valid_attributes.push("db.VarChar".to_string()); + constraints.valid_attributes.push("db.Text".to_string()); + } + ScalarType::Int | ScalarType::BigInt => { + constraints.valid_attributes.push("db.Integer".to_string()); + constraints + .valid_attributes + .push("autoincrement".to_string()); + } + ScalarType::DateTime => { + constraints.valid_attributes.push("updatedAt".to_string()); + } + _ => {} + } + + constraints + } +} + +/// Statistics about the type resolver. +#[derive(Debug, Clone)] +pub struct TypeResolverStats { + pub cached_types: usize, + pub dependency_edges: usize, + pub constraint_count: usize, +} + +/// Errors that can occur during type resolution. +#[derive(Debug, Clone)] +pub enum TypeResolutionError { + /// Unknown type name + UnknownType { name: String }, + + /// Symbol exists but is not a type + NotAType { + name: String, + actual_symbol_type: String, + }, + + /// Unknown built-in type + UnknownBuiltinType { name: String }, + + /// Circular type dependency + CircularDependency { cycle: Vec }, + + /// Type constraint violation + ConstraintViolation { + type_name: String, + constraint: String, + message: String, + }, + + /// Incompatible types + IncompatibleTypes { + from: String, + to: String, + context: String, + }, +} + +impl std::fmt::Display for TypeResolutionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TypeResolutionError::UnknownType { name } => { + write!(f, "Unknown type '{name}'") + } + TypeResolutionError::NotAType { + name, + actual_symbol_type, + } => { + write!( + f, + "'{name}' is not a type (it's a {actual_symbol_type})" + ) + } + TypeResolutionError::UnknownBuiltinType { name } => { + write!(f, "Unknown built-in type '{name}'") + } + TypeResolutionError::CircularDependency { cycle } => { + write!(f, "Circular type dependency: {}", cycle.join(" -> ")) + } + TypeResolutionError::ConstraintViolation { + type_name, + constraint, + message, + } => { + write!( + f, + "Type '{type_name}' violates constraint '{constraint}': {message}" + ) + } + TypeResolutionError::IncompatibleTypes { from, to, context } => { + write!( + f, + "Incompatible types: cannot convert '{from}' to '{to}' in {context}" + ) + } + } + } +} + +impl std::error::Error for TypeResolutionError {}