cargo-msrv 0.18.1

Find your minimum supported Rust version (MSRV)!
use camino::Utf8PathBuf;
use owo_colors::OwoColorize;
use std::env;
use std::ffi::OsString;
use std::io;
use std::path::PathBuf;
use std::string::FromUtf8Error;

use crate::cli::rust_releases_opts::{ParseEditionError, ParseEditionOrVersionError};
use crate::log_level::ParseLogLevelError;
use crate::manifest::bare_version::{BareVersion, NoVersionMatchesManifestMsrvError};
use crate::manifest::ManifestParseError;
use rust_releases::Release;

use crate::sub_command::{show, verify};

pub(crate) type TResult<T> = Result<T, CargoMSRVError>;

#[derive(Debug, thiserror::Error)]
pub enum CargoMSRVError {
    #[error("Unable to parse minimum rust version: {0}")]
    BareVersionParse(#[from] crate::manifest::bare_version::Error),

    #[error(transparent)]
    CargoMetadata(#[from] cargo_metadata::Error),

    #[error("The default host triple (target) could not be found.")]
    DefaultHostTripleNotFound,

    #[error(transparent)]
    Env(#[from] env::VarError),

    #[error("{0}")]
    GenericMessage(String),

    #[error(transparent)]
    Io(#[from] IoError),

    #[error("{0}")]
    InvalidConfig(String),

    #[error(transparent)]
    InvalidRustVersionNumber(#[from] std::num::ParseIntError),

    #[error(transparent)]
    InvalidMsrvSet(#[from] InvalidMsrvSetError),

    #[error(transparent)]
    InvalidUTF8(#[from] FromUtf8Error),

    #[error(transparent)]
    ManifestParseError(#[from] ManifestParseError),

    #[error("No crate root found for given crate")]
    NoCrateRootFound,

    #[error(transparent)]
    NoToolchainsToTry(#[from] NoToolchainsToTryError),

    #[error("Unable to set MSRV for workspace, try setting it for individual packages instead.")]
    WorkspaceFound,

    #[error(transparent)]
    NoVersionMatchesManifestMSRV(#[from] NoVersionMatchesManifestMsrvError),

    #[error("Unable to find key 'package.rust-version' (or 'package.metadata.msrv') in '{0}'")]
    NoMSRVKeyInCargoToml(Utf8PathBuf),

    #[error(transparent)]
    ParseEdition(#[from] ParseEditionError),

    #[error(transparent)]
    ParseEditionOrVersion(#[from] ParseEditionOrVersionError),

    #[error(transparent)]
    ParseLogLevel(#[from] ParseLogLevelError),

    #[error("Unable to parse Cargo.toml: {0}")]
    ParseToml(#[from] toml_edit::TomlError),

    #[error(transparent)]
    RustReleasesSource(#[from] rust_releases::RustChangelogError),

    #[error(transparent)]
    #[cfg(feature = "rust-releases-dist-source")]
    RustReleasesRustDistSource(#[from] rust_releases::RustDistError),

    #[error("Unable to parse rust-releases source from '{0}'")]
    RustReleasesSourceParseError(String),

    #[error("There are no Rust releases in the rust-releases index")]
    RustReleasesEmptyReleaseSet,

    #[error(transparent)]
    RustupError(#[from] RustupError),

    #[error("Check toolchain (with `rustup run <toolchain> <command>`) failed.")]
    RustupRunWithCommandFailed,

    #[error(transparent)]
    SemverError(#[from] rust_releases::semver::Error),

    #[error(transparent)]
    SetMsrv(#[from] SetMsrvError),

    #[error("Unable to print event output")]
    Storyteller,

    #[error(transparent)]
    SubCommandVerify(#[from] verify::Error),

    #[error(transparent)]
    SubCommandShow(#[from] show::Error),

    #[error(transparent)]
    SystemTime(#[from] std::time::SystemTimeError),

    #[error("The given toolchain could not be found. Run `rustup toolchain list` for an overview of installed toolchains.")]
    ToolchainNotInstalled,

    #[error("The given target could not be found. Run `rustup target list` for an overview of available toolchains.")]
    UnknownTarget,

    #[error("Unable to get or store the channel manifest on disk.")]
    UnableToCacheChannelManifest,

    #[error(
        r#"Unable to find a Minimum Supported Rust Version (MSRV).

If you think this result is erroneous, please run: `{command}` manually.

If the above does succeed, or you think cargo-msrv errored in another way, please feel free to
report the issue at: https://github.com/foresterre/cargo-msrv/issues

Thank you in advance!"#
    )]
    UnableToFindAnyGoodVersion { command: String },

    #[error("Unable to parse the CLI arguments. Use `cargo msrv help` for more info.")]
    UnableToParseCliArgs,

    #[error("The Rust stable version could not be parsed from the stable channel manifest.")]
    UnableToParseRustVersion,

    #[error("Unable to run the check command: '{}' at '{}'", &command, &cwd)]
    UnableToRunCheck { command: String, cwd: Utf8PathBuf },

    #[error(transparent)]
    Path(#[from] PathError),
}

impl From<String> for CargoMSRVError {
    fn from(s: String) -> Self {
        Self::GenericMessage(s)
    }
}

#[derive(Debug, thiserror::Error)]
#[error("IO error: '{error}'. caused by: '{source}'.")]
pub struct IoError {
    pub error: io::Error,
    pub source: IoErrorSource,
}

#[derive(Debug, thiserror::Error)]
pub enum IoErrorSource {
    #[error("Unable to determine current working directory")]
    CurrentDir,

    #[error("Unable to open file '{0}'")]
    OpenFile(Utf8PathBuf),

    #[error("Unable to read file '{0}'")]
    ReadFile(Utf8PathBuf),

    #[error("Unable to write file '{0}'")]
    WriteFile(Utf8PathBuf),

    #[error("Unable to remove file '{0}'")]
    RemoveFile(Utf8PathBuf),

    #[error("Unable to rename file '{0}'")]
    RenameFile(Utf8PathBuf),

    #[error("Unable to spawn process '{0:?}'")]
    SpawnProcess(OsString),

    #[error("Unable to collect output from '{0:?}', or process did not terminate properly")]
    WaitForProcessAndCollectOutput(OsString),
}

#[derive(Debug, thiserror::Error)]
pub enum SetMsrvError {
    #[error(
        "Unable to set the MSRV in the 'package.metadata' table: 'package.metadata' is not a table"
    )]
    NotATable,
}

#[derive(Debug, thiserror::Error)]
#[error("No Rust releases to check: the filtered search space is empty.{}",
    inner.as_ref().map(|clues| format!("{}", clues)).unwrap_or_default(),
)]
pub struct NoToolchainsToTryError {
    inner: Option<NoToolchainToTryClues>,
}

impl NoToolchainsToTryError {
    pub fn new_empty() -> Self {
        Self { inner: None }
    }

    pub fn with_clues(user_min: Option<BareVersion>, user_max: Option<BareVersion>) -> Self {
        Self {
            inner: Some(NoToolchainToTryClues {
                min: user_min,
                max: user_max,
            }),
        }
    }

    pub fn has_clues(&self) -> bool {
        self.inner.is_some()
    }
}

#[derive(Debug, thiserror::Error)]
#[error(" Search space limited by user to min Rust '{}', and max Rust '{}'",
    min.as_ref().map(|s| format!("{}", s)).unwrap_or_else(|| "<not overridden>".to_string()),
    max.as_ref().map(|s| format!("{}", s)).unwrap_or_else(|| "<not overridden>".to_string()),
)]
pub struct NoToolchainToTryClues {
    min: Option<BareVersion>,
    max: Option<BareVersion>,
}

#[derive(Debug, thiserror::Error)]
#[error("No Rust releases match input '{}' (search space: [{}])",
    input,
    search_space.iter().map(|r| r.version().to_string()).collect::<Vec<_>>().join(", ") )
]
pub struct InvalidMsrvSetError {
    pub(crate) input: BareVersion,
    pub(crate) search_space: Vec<Release>,
}

impl<T> From<storyteller::EventReporterError<T>> for CargoMSRVError {
    fn from(_: storyteller::EventReporterError<T>) -> Self {
        CargoMSRVError::Storyteller
    }
}

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum RustupError {
    Install(#[from] RustupInstallError),
    AddComponent(#[from] RustupAddComponentError),
    AddTarget(#[from] RustupAddTargetError),
}

#[derive(Debug, thiserror::Error)]
#[error(
    "Unable to install toolchain '{}', rustup reported:\n    {}",
    toolchain_spec,
    stderr.trim_end().lines().collect::<Vec<_>>().join("\n    ").dimmed()
)]
pub struct RustupInstallError {
    pub toolchain_spec: String,
    pub stderr: String,
}

#[derive(Debug, thiserror::Error)]
#[error(
    "Unable to add components '{}' to toolchain '{}', rustup reported:\n    {}",
    components,
    toolchain_spec,
    stderr.trim_end().lines().collect::<Vec<_>>().join("\n    ").dimmed()
)]
pub struct RustupAddComponentError {
    pub components: String,
    pub toolchain_spec: String,
    pub stderr: String,
}

#[derive(Debug, thiserror::Error)]
#[error(
    "Unable to add target '{}' to toolchain '{}', rustup reported:\n    {}",
    targets,
    toolchain_spec,
    stderr.trim_end().lines().collect::<Vec<_>>().join("\n    ").dimmed()
)]
pub struct RustupAddTargetError {
    pub targets: String,
    pub toolchain_spec: String,
    pub stderr: String,
}

#[derive(Debug, thiserror::Error)]
pub enum PathError {
    #[error("'{}' does not exist", .0.display())]
    DoesNotExist(PathBuf),

    #[error("No parent directory for '{}'", .0.display())]
    NoParent(PathBuf),

    #[error(transparent)]
    InvalidUtf8(#[from] InvalidUtf8Error),
}

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct InvalidUtf8Error {
    error: Utf8PathErrorInner,
}

impl From<camino::FromPathError> for InvalidUtf8Error {
    fn from(value: camino::FromPathError) -> Self {
        Self {
            error: Utf8PathErrorInner::FromPath(value),
        }
    }
}

impl From<camino::FromPathBufError> for InvalidUtf8Error {
    fn from(value: camino::FromPathBufError) -> Self {
        Self {
            error: Utf8PathErrorInner::FromPathBuf(value),
        }
    }
}

#[derive(Debug, thiserror::Error)]
enum Utf8PathErrorInner {
    #[error("Path contains non UTF-8 characters")]
    FromPath(camino::FromPathError),
    #[error("Path contains non UTF-8 characters (path: '{}')", .0.as_path().display())]
    FromPathBuf(camino::FromPathBufError),
}