ryo-symbol 0.1.0

Symbol system for Rust codebase - unique identifiers and file path management
Documentation
//! Error types for ryo-symbol

use std::path::PathBuf;
use thiserror::Error;
use uuid::Uuid;

use crate::id::SymbolId;
use crate::kind::SymbolKind;
use crate::path::SymbolPath;

/// SymbolPath parse error
#[derive(Debug, Clone, Error)]
pub enum ParseError {
    /// The input string was empty (or contained only separators).
    #[error("empty path")]
    Empty,

    /// A path segment was not a valid Rust identifier. Carries the offending
    /// segment.
    #[error("invalid identifier: {0}")]
    InvalidIdentifier(String),

    /// The overall path layout was malformed (e.g. leading/trailing `::`,
    /// empty segment between `::`). Carries the offending input.
    #[error("invalid path format: {0}")]
    InvalidFormat(String),

    #[error(
        "semantic keyword '{0}' not allowed in SymbolPath\n\n\
        SymbolPath requires canonical (absolute) paths. Context-dependent keywords like 'crate', 'self', 'super' \
        cannot be used because they depend on the current module context.\n\n\
        Why canonical paths?\n\
        - Rust's AST uses relative keywords (crate::, self::, super::)\n\
        - Ryo's SymbolPath uses absolute paths for:\n\
          • Fast lookup in SymbolRegistry (HashMap key)\n\
          • Unambiguous mutation targeting across files\n\
          • Efficient AST updates without context re-resolution\n\n\
        Solutions:\n\
        1. In tests: Use actual crate name\n\
           ✗ SymbolPath::parse(\"crate::Config\")\n\
           ✓ SymbolPath::parse(\"test_crate::Config\")\n\n\
        2. In implementation: Resolve context first\n\
           • Use SymbolResolver to resolve 'crate' to actual crate name\n\
           • Get canonical path from AnalysisContext/SymbolRegistry\n\
           • Pass canonical path from parent context\n\n\
        3. Design consideration:\n\
           If you cannot determine the canonical path at this point,\n\
           refactor the API to receive it from a higher context that has the information."
    )]
    /// A context-dependent keyword (`crate`, `self`, `super`) appeared in a
    /// path that requires canonical (absolute) form. Carries the offending
    /// keyword; the variant's `#[error]` message walks the caller through
    /// how to resolve to a canonical name.
    SemanticKeyword(String),

    /// The leading crate segment did not match any registered crate. Carries
    /// the full path, the unrecognized crate segment, and a rendered list of
    /// known crate names for diagnostics.
    #[error("unknown crate '{crate_name}' in path '{path}'. Known crates: [{known}]")]
    UnknownCrate {
        /// Full SymbolPath input that triggered the error.
        path: String,
        /// The leading segment that did not match any known crate.
        crate_name: String,
        /// Comma-separated rendering of currently registered crate names.
        known: String,
    },
}

/// Symbol registration error
#[derive(Debug, Clone, Error)]
pub enum RegistrationError {
    /// The supplied symbol path failed to parse; wraps the underlying
    /// [`ParseError`].
    #[error("invalid path: {0}")]
    InvalidPath(#[from] ParseError),

    /// A symbol with the same path was already registered with a different
    /// [`SymbolKind`]; re-registration with a conflicting kind is rejected.
    #[error("conflicting kind: {path} already registered as {existing:?}, got {new:?}")]
    ConflictingKind {
        /// The path that was being re-registered.
        path: Box<SymbolPath>,
        /// Kind already recorded in the registry.
        existing: SymbolKind,
        /// Kind requested by the new registration.
        new: SymbolKind,
    },

    /// The declared parent symbol does not exist or is not a valid parent
    /// for the symbol being registered.
    #[error("invalid parent symbol")]
    InvalidParent,

    /// Two distinct UUIDs were observed for the same [`SymbolId`], which
    /// must not happen under normal operation.
    #[error("UUID conflict for {id:?}: existing={existing}, provided={provided}")]
    UuidConflict {
        /// The symbol whose UUID disagreed.
        id: SymbolId,
        /// UUID already stored in the registry.
        existing: Uuid,
        /// UUID provided by the new registration attempt.
        provided: Uuid,
    },
}

/// Re-export unregistration error
#[derive(Debug, Clone, Error)]
pub enum UnregisterReexportError {
    /// The alias path was not found in the re-export registry.
    #[error("alias path not found in re-export registry")]
    NotFound,
}

/// Invalid SymbolId error
///
/// This is a **fatal error**. When encountered:
/// - Immediately return Err and abort the current Tick
/// - Propagate to upper LLM for re-planning
/// - Never attempt to continue with corrupted state
#[derive(Debug, Clone, Error)]
#[error("invalid symbol id: {0:?}")]
pub struct InvalidSymbolId(pub SymbolId);

/// Symbol rename error
#[derive(Debug, Clone, Error)]
pub enum RenameError {
    /// The supplied [`SymbolId`] does not exist in the registry.
    #[error("invalid symbol id: {0:?}")]
    InvalidId(SymbolId),

    /// The target path is already occupied by another symbol, so the rename
    /// would collide.
    #[error("path already exists: {0}")]
    PathExists(Box<SymbolPath>),
}

/// Path resolution error (for local paths like `crate::`, `self::`, `super::`)
#[derive(Debug, Clone, Error)]
pub enum ResolutionError {
    /// The leading `crate` segment could not be resolved to an actual crate
    /// name in the current context. Carries the path that triggered the
    /// failure.
    #[error("unresolved crate: {0}")]
    UnresolvedCrate(String),

    /// A `self` / inner module reference could not be resolved against the
    /// active module hierarchy. Carries the unresolved path.
    #[error("unresolved module: {0}")]
    UnresolvedModule(String),

    /// `super` was used at the crate root, where it has no parent to refer
    /// to.
    #[error("super at crate root")]
    SuperAtRoot,

    /// Resolution succeeded structurally but the named symbol does not
    /// exist in the registry. Carries the offending path.
    #[error("symbol not found: {0}")]
    SymbolNotFound(String),

    /// The symbol exists but no source-span information is available, so
    /// the requested location cannot be produced.
    #[error("no span info for symbol: {0}")]
    NoSpanInfo(String),
}

/// WorkspacePathResolver error
#[derive(Debug, Error)]
pub enum ResolveError {
    /// The requested path lives outside the active workspace root, so it
    /// cannot be resolved to a crate-relative location.
    #[error("path '{}' is outside workspace '{}'", path.display(), workspace.display())]
    OutsideWorkspace {
        /// Requested filesystem path.
        path: PathBuf,
        /// Workspace root the resolver is currently scoped to.
        workspace: PathBuf,
    },

    /// The requested file does not exist on disk.
    #[error("file not found: '{}'", .0.display())]
    FileNotFound(PathBuf),

    /// No crate in the workspace owns the requested path (e.g. the file is
    /// inside the workspace root but not under any crate's `src/`).
    #[error("crate not found for path: '{}'", .0.display())]
    CrateNotFound(PathBuf),

    /// Underlying I/O failure while walking the workspace.
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),

    /// Ambiguous `crate::` path in multi-crate workspace
    ///
    /// In a workspace with multiple crates, `crate::xxx` is ambiguous because
    /// it's unclear which crate's `xxx` is being referred to.
    #[error(
        "ambiguous path '{path}' in multi-crate workspace\n\n\
        In a workspace with multiple crates, 'crate::' prefix is ambiguous.\n\n\
        Solutions:\n\
        1. Specify the crate with -p flag: ryo run -p {example_crate_path} -f intent.json\n\
        2. Use file path instead: \"parent\": \"{example_file_path}\"\n\
        3. Use explicit crate name: \"parent\": \"{example_crate_name}::xxx\""
    )]
    AmbiguousCratePath {
        /// The ambiguous path (e.g., "crate::domain")
        path: String,
        /// Example crate path for -p flag (e.g., "crates/core")
        example_crate_path: String,
        /// Example file path (e.g., "crates/core/src/domain.rs")
        example_file_path: String,
        /// Example crate name (e.g., "core")
        example_crate_name: String,
    },
}