diff --git a/Cargo.toml b/Cargo.toml index 5f271f86..e390a9f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,7 +116,9 @@ redundant_type_annotations = "warn" renamed_function_params = "warn" semicolon_outside_block = "warn" string_to_string = "warn" +todo = "deny" undocumented_unsafe_blocks = "warn" +unimplemented = "deny" unnecessary_safety_comment = "warn" unnecessary_safety_doc = "warn" unneeded_field_pattern = "warn" diff --git a/crates/ohno/examples/unimplemented.rs b/crates/ohno/examples/unimplemented.rs new file mode 100644 index 00000000..816a7eb5 --- /dev/null +++ b/crates/ohno/examples/unimplemented.rs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![expect(clippy::unwrap_used, reason = "example code")] + +use ohno::{Unimplemented, unimplemented_error}; + +#[ohno::error] +#[from(Unimplemented)] +pub struct MyError; + +fn do_something(is_lucky: bool) -> Result<(), MyError> { + if is_lucky { + Ok(()) + } else { + unimplemented_error!("this feature is not yet implemented"); + } +} + +fn main() { + let err = do_something(false).unwrap_err(); + println!("Error: {err}"); +} + +// Output: +// Error: not implemented at crates\ohno\examples\unimplemented.rs:11 +// caused by: this feature is not yet implemented diff --git a/crates/ohno/src/lib.rs b/crates/ohno/src/lib.rs index dcfb6301..51c1dbb0 100644 --- a/crates/ohno/src/lib.rs +++ b/crates/ohno/src/lib.rs @@ -235,13 +235,14 @@ mod error_ext; mod error_trace; mod source; mod trace_info; +mod unimplemented; #[cfg(any(feature = "test-util", test))] pub mod test_util; pub use core::OhnoCore; - pub use error_ext::ErrorExt; pub use error_trace::{ErrorTrace, ErrorTraceExt}; pub use ohno_macros::{Error, error, error_trace}; pub use trace_info::{Location, TraceInfo}; +pub use unimplemented::Unimplemented; diff --git a/crates/ohno/src/unimplemented.rs b/crates/ohno/src/unimplemented.rs new file mode 100644 index 00000000..cb73d8c6 --- /dev/null +++ b/crates/ohno/src/unimplemented.rs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! `Unimplemented` error type. + +use std::borrow::Cow; + +use crate::OhnoCore; + +/// Error type for unimplemented functionality. +/// +/// This type is designed to replace panicking macros like [`todo!`] and +/// [`unimplemented!`] with a proper error that can be handled gracefully. +/// +/// See the documentation for the [`unimplemented_error!`](crate::unimplemented_error!) macro for +/// more details. +/// +/// # Examples +/// +/// ``` +/// use ohno::{Unimplemented, unimplemented_error}; +/// +/// fn not_ready_yet() -> Result<(), Unimplemented> { +/// unimplemented_error!("this feature is coming soon") +/// } +/// ``` +#[derive(crate::Error, Clone)] +#[no_constructors] +#[display("not implemented at {file}:{line}")] +pub struct Unimplemented { + file: Cow<'static, str>, + line: u32, + core: OhnoCore, +} + +impl Unimplemented { + /// Creates a new `Unimplemented` error. + #[must_use] + pub fn new(file: Cow<'static, str>, line: u32) -> Self { + Self { + file, + line, + core: OhnoCore::new(), + } + } + + /// Creates a new `Unimplemented` error with a custom message. + /// + /// The message provides additional context about why the functionality + /// is not yet implemented or what needs to be done. + #[must_use] + pub fn with_message(message: impl Into>, file: Cow<'static, str>, line: u32) -> Self { + Self { + file, + line, + core: OhnoCore::from(message.into()), + } + } + + /// Returns the file path where this error was created. + #[must_use] + pub fn file(&self) -> &str { + &self.file + } + + /// Returns the line number where this error was created. + #[must_use] + pub fn line(&self) -> u32 { + self.line + } +} + +/// Returns an [`Unimplemented`] error from the current function. +/// +/// This macro is designed to replace panicking macros like [`todo!`] and +/// [`unimplemented!`] with a proper error that can be handled gracefully. +/// It automatically captures the file and line information and returns early +/// with an `Unimplemented` error. +/// +/// Unlike the standard panicking macros, this allows your application to: +/// +/// - Continue running and handle the error appropriately +/// - Log the error with full context (file, line, message) +/// - Return meaningful error responses to users instead of crashing +/// - Test error paths without triggering panics +/// +/// To prevent accidental use of panicking macros, enable these clippy lints: +/// +/// ```toml +/// [workspace.lints.clippy] +/// todo = "deny" +/// unimplemented = "deny" +/// ``` +/// +/// The error can be automatically converted into any error type that implements +/// `From`, making it easy to use in functions with different +/// error types. +/// +/// # Examples +/// +/// Basic usage without a message: +/// +/// ``` +/// # use ohno::unimplemented_error; +/// fn future_feature() -> Result { +/// unimplemented_error!() +/// } +/// ``` +/// +/// With a custom message: +/// +/// ``` +/// # use ohno::unimplemented_error; +/// fn experimental_api() -> Result<(), ohno::Unimplemented> { +/// unimplemented_error!("async runtime support not yet available") +/// } +/// ``` +/// +/// Automatic conversion to custom error types: +/// +/// ``` +/// # use ohno::{unimplemented_error, Unimplemented}; +/// #[ohno::error] +/// #[from(Unimplemented)] +/// struct AppError; +/// +/// fn app_function() -> Result<(), AppError> { +/// unimplemented_error!("feature coming in v2.0") +/// } +/// ``` +#[macro_export] +macro_rules! unimplemented_error { + () => { + return Err($crate::Unimplemented::new(::std::borrow::Cow::Borrowed(file!()), line!()).into()) + }; + ($ex:expr) => { + return Err($crate::Unimplemented::with_message($ex, ::std::borrow::Cow::Borrowed(file!()), line!()).into()) + }; +} + +#[cfg(test)] +mod test { + use ohno::ErrorExt; + + use super::*; + + #[test] + fn basic() { + fn return_err() -> Result<(), Unimplemented> { + unimplemented_error!() + } + let err = return_err().unwrap_err(); + assert!(err.message().starts_with("not implemented at "), "{err}"); + } + + #[test] + fn with_message() { + fn return_err() -> Result<(), Unimplemented> { + unimplemented_error!("custom message") + } + + let err = return_err().unwrap_err(); + let message = err.message(); + assert!(message.starts_with("not implemented at "), "{message}"); + assert!(message.contains("custom message"), "{message}"); + } + + #[test] + fn file_and_line() { + let err = Unimplemented::new("file.rs".into(), 111); + assert_eq!(err.file(), "file.rs"); + assert_eq!(err.line(), 111); + } + + #[test] + fn automatic_conversion() { + #[derive(Debug)] + struct CustomError(Unimplemented); + + impl From for CustomError { + fn from(err: Unimplemented) -> Self { + Self(err) + } + } + + fn return_custom_err() -> Result<(), CustomError> { + unimplemented_error!() + } + + let err = return_custom_err().unwrap_err(); + let message = err.0.message(); + assert!(message.starts_with("not implemented at "), "{message}"); + } +}