maya-mel 0.1.2

Single-entry Autodesk Maya MEL parsing and analysis library.
Documentation
#![forbid(unsafe_code)]
//! Generic semantic analysis for MEL syntax trees.
//!
//! Most users should start with [`analyze`]. It resolves proc and variable
//! usage, emits diagnostics, and optionally normalizes command-style invokes
//! through a caller-provided [`command_schema::CommandRegistry`]. Advanced
//! command contracts live under [`command_schema`], and normalized command
//! shapes live under [`command_norm`].

/// Advanced command normalization data structures.
pub mod command_norm;
/// Advanced command schema and registry contracts.
pub mod command_schema;
mod flow;
mod resolve;
pub(crate) mod scope;

#[cfg(test)]
mod tests;

pub use command_norm::NormalizedCommandInvoke;
use command_schema::{CommandRegistry, EmptyCommandRegistry};

use flow::FlowLintAnalyzer;
use resolve::Analyzer;
use scope::ScopeCollector;
use std::sync::Arc;

use mel_ast::{SourceFile, Stmt};
use mel_syntax::{SourceView, TextRange};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Diagnostic severity emitted by semantic analysis.
pub enum DiagnosticSeverity {
    Error,
    Warning,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Filter used by diagnostics-only semantic entry points.
pub enum DiagnosticFilter {
    All,
    ErrorsOnly,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// Semantic diagnostic with primary and secondary labels.
///
/// These diagnostics are produced by [`analyze`] and the diagnostics-only entry
/// points in this module.
pub struct Diagnostic {
    pub severity: DiagnosticSeverity,
    pub message: Arc<str>,
    pub range: TextRange,
    pub labels: Vec<DiagnosticLabel>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// A labeled span attached to a [`Diagnostic`].
pub struct DiagnosticLabel {
    pub range: TextRange,
    pub message: Arc<str>,
    pub is_primary: bool,
}

impl Diagnostic {
    fn error(message: impl Into<Arc<str>>, range: TextRange) -> Self {
        let message = message.into();
        Self {
            severity: DiagnosticSeverity::Error,
            message: message.clone(),
            range,
            labels: vec![DiagnosticLabel {
                range,
                message,
                is_primary: true,
            }],
        }
    }

    fn warning(message: impl Into<Arc<str>>, range: TextRange) -> Self {
        let message = message.into();
        Self {
            severity: DiagnosticSeverity::Warning,
            message: message.clone(),
            range,
            labels: vec![DiagnosticLabel {
                range,
                message,
                is_primary: true,
            }],
        }
    }

    fn with_secondary_label(mut self, message: impl Into<Arc<str>>, range: TextRange) -> Self {
        self.labels.push(DiagnosticLabel {
            range,
            message: message.into(),
            is_primary: false,
        });
        self
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Stable identifier for a collected lexical scope.
pub struct ScopeId(pub(crate) usize);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Stable identifier for a collected proc symbol.
pub struct ProcSymbolId(pub(crate) usize);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Stable identifier for a collected variable symbol.
pub struct VariableSymbolId(pub(crate) usize);

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcSymbol {
    pub id: ProcSymbolId,
    pub name_range: TextRange,
    pub is_global: bool,
    pub return_type: Option<mel_ast::ProcReturnType>,
    pub owner_scope: ScopeId,
    pub decl_order: usize,
    pub range: TextRange,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VariableKind {
    Parameter,
    Local,
    Global,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VariableSymbol {
    pub id: VariableSymbolId,
    pub name_range: TextRange,
    pub kind: VariableKind,
    pub ty: mel_ast::TypeName,
    pub is_array: bool,
    pub owner_scope: ScopeId,
    pub decl_order: usize,
    pub range: TextRange,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvokeResolution {
    pub range: TextRange,
    pub scope: ScopeId,
    pub resolution: ResolvedCallee,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedCallee {
    Unresolved,
    Proc(ProcSymbolId),
    BuiltinCommand(Arc<str>),
    PluginCommand(Arc<str>),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentTarget {
    Unresolved,
    Variable(VariableSymbolId),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentResolution {
    pub range: TextRange,
    pub scope: ScopeId,
    pub name_range: TextRange,
    pub resolution: IdentTarget,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
/// Full semantic analysis result.
///
/// This bundles semantic diagnostics together with proc, variable, and invoke
/// resolution data so downstream tools can inspect more than just errors.
pub struct Analysis {
    pub diagnostics: Vec<Diagnostic>,
    pub proc_symbols: Vec<ProcSymbol>,
    pub variable_symbols: Vec<VariableSymbol>,
    pub invoke_resolutions: Vec<InvokeResolution>,
    pub ident_resolutions: Vec<IdentResolution>,
    pub normalized_invokes: Vec<NormalizedCommandInvoke>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AnalysisMode {
    Full,
    DiagnosticsOnly,
}

#[must_use]
/// Run semantic analysis without a command registry.
///
/// ```rust
/// use maya_mel::{analyze, parse_source};
///
/// let parse = parse_source("global proc hello() {} hello();");
/// let analysis = analyze(&parse.syntax, parse.source_view());
///
/// assert!(analysis.diagnostics.is_empty());
/// assert_eq!(analysis.proc_symbols.len(), 1);
/// ```
pub fn analyze(syntax: &SourceFile, source: SourceView<'_>) -> Analysis {
    analyze_with_registry(syntax, source, &EmptyCommandRegistry)
}

#[must_use]
/// Run semantic analysis with a caller-provided command registry.
///
/// ```rust
/// use maya_mel::{MayaCommandRegistry, analyze_with_registry, parse_source};
///
/// let parse = parse_source("createNode transform -n \"root\";");
/// let analysis = analyze_with_registry(&parse.syntax, parse.source_view(), &MayaCommandRegistry::new());
///
/// assert!(analysis.diagnostics.is_empty());
/// assert!(!analysis.normalized_invokes.is_empty());
/// ```
pub fn analyze_with_registry<R>(
    syntax: &SourceFile,
    source: SourceView<'_>,
    registry: &R,
) -> Analysis
where
    R: CommandRegistry + ?Sized,
{
    analyze_with_registry_mode(
        syntax,
        source,
        registry,
        AnalysisMode::Full,
        DiagnosticFilter::All,
    )
}

#[must_use]
/// Collect only diagnostics while still using a command registry.
pub fn analyze_diagnostics_with_registry<R>(
    syntax: &SourceFile,
    source: SourceView<'_>,
    registry: &R,
) -> Vec<Diagnostic>
where
    R: CommandRegistry + ?Sized,
{
    analyze_diagnostics_with_registry_filtered(syntax, source, registry, DiagnosticFilter::All)
}

#[must_use]
/// Collect only diagnostics with an explicit [`DiagnosticFilter`].
pub fn analyze_diagnostics_with_registry_filtered<R>(
    syntax: &SourceFile,
    source: SourceView<'_>,
    registry: &R,
    filter: DiagnosticFilter,
) -> Vec<Diagnostic>
where
    R: CommandRegistry + ?Sized,
{
    analyze_with_registry_mode(
        syntax,
        source,
        registry,
        AnalysisMode::DiagnosticsOnly,
        filter,
    )
    .diagnostics
}

fn analyze_with_registry_mode<R>(
    syntax: &SourceFile,
    source: SourceView<'_>,
    registry: &R,
    mode: AnalysisMode,
    filter: DiagnosticFilter,
) -> Analysis
where
    R: CommandRegistry + ?Sized,
{
    let collected = ScopeCollector::collect(syntax);
    let mut analyzer = Analyzer::new(
        &collected,
        source,
        registry,
        matches!(mode, AnalysisMode::Full),
        filter,
    );

    for item in &syntax.items {
        analyzer.walk_item(item, collected.root_scope);
    }

    let mut diagnostics = analyzer.diagnostics;
    if matches!(filter, DiagnosticFilter::All) {
        let mut flow_lint = FlowLintAnalyzer::new(&collected, source);
        flow_lint.walk_source(syntax);
        diagnostics.extend(flow_lint.diagnostics);
    }

    Analysis {
        diagnostics,
        proc_symbols: if matches!(mode, AnalysisMode::Full) {
            collected.proc_symbols.clone()
        } else {
            Vec::new()
        },
        variable_symbols: if matches!(mode, AnalysisMode::Full) {
            collected.variable_symbols.clone()
        } else {
            Vec::new()
        },
        invoke_resolutions: if matches!(mode, AnalysisMode::Full) {
            analyzer.invoke_resolutions
        } else {
            Vec::new()
        },
        ident_resolutions: if matches!(mode, AnalysisMode::Full) {
            analyzer.ident_resolutions
        } else {
            Vec::new()
        },
        normalized_invokes: if matches!(mode, AnalysisMode::Full) {
            analyzer.normalized_invokes
        } else {
            Vec::new()
        },
    }
}

pub(crate) fn stmt_range(stmt: &Stmt) -> TextRange {
    match stmt {
        Stmt::Empty { range }
        | Stmt::Proc { range, .. }
        | Stmt::Block { range, .. }
        | Stmt::Expr { range, .. }
        | Stmt::VarDecl { range, .. }
        | Stmt::If { range, .. }
        | Stmt::While { range, .. }
        | Stmt::DoWhile { range, .. }
        | Stmt::Switch { range, .. }
        | Stmt::For { range, .. }
        | Stmt::ForIn { range, .. }
        | Stmt::Return { range, .. }
        | Stmt::Break { range }
        | Stmt::Continue { range } => *range,
    }
}