mdbook_lint_core/
error.rs

1//! Error types for mdbook-lint
2//!
3//! This module provides a comprehensive error system that replaces the use of `anyhow::Error`
4//! with structured, programmatically-handleable error types. This enables better API design
5//! and error recovery for library consumers.
6//!
7//! # Architecture Overview
8//!
9//! The error system is built around several key principles:
10//!
11//! 1. **Structured Errors**: Each error variant contains specific contextual information
12//! 2. **Error Categories**: Errors are grouped by domain (Rule, Document, Config, etc.)
13//! 3. **Conversion Traits**: Seamless conversion between specialized and general error types
14//! 4. **Context Addition**: Rich context can be added at any point in the error chain
15//! 5. **Compatibility**: Smooth migration path from `anyhow` with compatibility layers
16//!
17//! # Error Hierarchy
18//!
19//! ```text
20//! MdBookLintError (main error type)
21//! ├── Io(std::io::Error)              - File system operations
22//! ├── Parse { line, column, message } - Document parsing errors
23//! ├── Config(String)                  - Configuration issues
24//! ├── Rule { rule_id, message }       - Rule execution problems
25//! ├── Plugin(String)                  - Plugin system errors
26//! ├── Document(String)                - Document processing errors
27//! ├── Registry(String)                - Rule registry operations
28//! ├── Json(serde_json::Error)         - JSON serialization errors
29//! ├── Yaml(serde_yaml::Error)         - YAML serialization errors
30//! └── Toml(toml::de::Error)           - TOML serialization errors
31//! ```
32//!
33//! # Usage Examples
34//!
35//! ## Basic Error Creation
36//!
37//! ```rust
38//! use mdbook_lint_core::error::{MdBookLintError, Result};
39//!
40//! // Create specific errors
41//! let parse_err = MdBookLintError::parse_error(10, 5, "Invalid syntax");
42//! let rule_err = MdBookLintError::rule_error("MD001", "Heading increment violation");
43//! let config_err = MdBookLintError::config_error("Invalid rule configuration");
44//! ```
45//!
46//! ## Error Matching and Handling
47//!
48//! ```rust
49//! use mdbook_lint_core::error::MdBookLintError;
50//!
51//! fn handle_error(err: MdBookLintError) {
52//!     match err {
53//!         MdBookLintError::Parse { line, column, message } => {
54//!             eprintln!("Parse error at {}:{}: {}", line, column, message);
55//!         }
56//!         MdBookLintError::Rule { rule_id, message } => {
57//!             eprintln!("Rule {} failed: {}", rule_id, message);
58//!         }
59//!         MdBookLintError::Io(io_err) => {
60//!             eprintln!("IO error: {}", io_err);
61//!         }
62//!         _ => eprintln!("Unknown error: {}", err),
63//!     }
64//! }
65//! ```
66//!
67//! ## Adding Context
68//!
69//! ```rust
70//! use mdbook_lint_core::error::{ErrorContext, Result};
71//!
72//! fn process_rule() -> Result<()> {
73//!     // ... some operation that might fail
74//!     # Ok(())
75//! }
76//!
77//! let result = process_rule().with_rule_context("MD001");
78//! ```
79//!
80//! ## Specialized Error Types
81//!
82//! For domain-specific operations, use the specialized error types:
83//!
84//! ```rust
85//! use mdbook_lint_core::error::{RuleError, DocumentError, IntoMdBookLintError};
86//!
87//! // Create specialized errors
88//! let rule_err = RuleError::not_found("MD999");
89//! let doc_err = DocumentError::too_large(1000000, 500000);
90//!
91//! // Convert to main error type
92//! let result: std::result::Result<(), _> = Err(rule_err);
93//! let main_result = result.into_mdbook_lint_error();
94//! ```
95//!
96//! # Migration from anyhow
97//!
98//! This module provides compatibility layers to ease migration:
99//!
100//! 1. **From<anyhow::Error>**: Convert anyhow errors to MdBookLintError
101//! 2. **Blanket conversion**: MdBookLintError implements std::error::Error, so anyhow can convert it back
102//! 3. **Result type alias**: Drop-in replacement for anyhow::Result
103//!
104//! # Performance Considerations
105//!
106//! - **Zero-cost when successful**: Error types only allocate when errors occur
107//! - **Structured data**: No string parsing needed to extract error information
108//! - **Efficient matching**: Enum variants enable efficient error handling
109//! - **Context preservation**: Rich context without performance penalty in success cases
110
111use thiserror::Error;
112
113/// Main error type for mdbook-lint operations
114#[derive(Debug, Error)]
115pub enum MdBookLintError {
116    /// IO-related errors (file reading, writing, etc.)
117    #[error("IO error: {0}")]
118    Io(#[from] std::io::Error),
119
120    /// Parsing errors with position information
121    #[error("Parse error at line {line}, column {column}: {message}")]
122    Parse {
123        line: usize,
124        column: usize,
125        message: String,
126    },
127
128    /// Configuration-related errors
129    #[error("Configuration error: {0}")]
130    Config(String),
131
132    /// Rule execution errors
133    #[error("Rule error in {rule_id}: {message}")]
134    Rule { rule_id: String, message: String },
135
136    /// Plugin system errors
137    #[error("Plugin error: {0}")]
138    Plugin(String),
139
140    /// Document processing errors
141    #[error("Document error: {0}")]
142    Document(String),
143
144    /// Registry operation errors
145    #[error("Registry error: {0}")]
146    Registry(String),
147
148    /// JSON serialization/deserialization errors
149    #[error("JSON error: {0}")]
150    Json(#[from] serde_json::Error),
151
152    /// YAML serialization/deserialization errors
153    #[error("YAML error: {0}")]
154    Yaml(#[from] serde_yaml::Error),
155
156    /// TOML serialization/deserialization errors
157    #[error("TOML error: {0}")]
158    Toml(#[from] toml::de::Error),
159
160    /// Directory traversal errors
161    #[error("Directory traversal error: {0}")]
162    WalkDir(#[from] walkdir::Error),
163}
164
165/// Specialized error type for rule-related operations
166#[derive(Debug, Error)]
167pub enum RuleError {
168    /// Rule not found in registry
169    #[error("Rule not found: {rule_id}")]
170    NotFound { rule_id: String },
171
172    /// Rule execution failed
173    #[error("Rule execution failed: {message}")]
174    ExecutionFailed { message: String },
175
176    /// Invalid rule configuration
177    #[error("Invalid rule configuration: {message}")]
178    InvalidConfig { message: String },
179
180    /// Rule dependency not met
181    #[error("Rule dependency not satisfied: {rule_id} requires {dependency}")]
182    DependencyNotMet { rule_id: String, dependency: String },
183
184    /// Rule registration conflict
185    #[error("Rule registration conflict: rule {rule_id} already exists")]
186    RegistrationConflict { rule_id: String },
187}
188
189/// Specialized error type for document-related operations
190#[derive(Debug, Error)]
191pub enum DocumentError {
192    /// Failed to read document from file
193    #[error("Failed to read document: {path}")]
194    ReadFailed { path: String },
195
196    /// Document format is invalid or unsupported
197    #[error("Invalid document format")]
198    InvalidFormat,
199
200    /// Document exceeds size limits
201    #[error("Document too large: {size} bytes (max: {max_size})")]
202    TooLarge { size: usize, max_size: usize },
203
204    /// Document parsing failed
205    #[error("Failed to parse document: {reason}")]
206    ParseFailed { reason: String },
207
208    /// Invalid encoding detected
209    #[error("Invalid encoding in document: {path}")]
210    InvalidEncoding { path: String },
211}
212
213/// Specialized error type for configuration operations
214#[derive(Debug, Error)]
215pub enum ConfigError {
216    /// Configuration file not found
217    #[error("Configuration file not found: {path}")]
218    NotFound { path: String },
219
220    /// Invalid configuration format
221    #[error("Invalid configuration format: {message}")]
222    InvalidFormat { message: String },
223
224    /// Configuration validation failed
225    #[error("Configuration validation failed: {field} - {message}")]
226    ValidationFailed { field: String, message: String },
227
228    /// Unsupported configuration version
229    #[error("Unsupported configuration version: {version} (supported: {supported})")]
230    UnsupportedVersion { version: String, supported: String },
231}
232
233/// Specialized error type for plugin operations
234#[derive(Debug, Error)]
235pub enum PluginError {
236    /// Plugin not found
237    #[error("Plugin not found: {plugin_id}")]
238    NotFound { plugin_id: String },
239
240    /// Plugin loading failed
241    #[error("Failed to load plugin {plugin_id}: {reason}")]
242    LoadFailed { plugin_id: String, reason: String },
243
244    /// Plugin initialization failed
245    #[error("Plugin initialization failed: {plugin_id}")]
246    InitializationFailed { plugin_id: String },
247
248    /// Plugin version incompatibility
249    #[error("Plugin version incompatible: {plugin_id} version {version} (required: {required})")]
250    VersionIncompatible {
251        plugin_id: String,
252        version: String,
253        required: String,
254    },
255}
256
257/// Result type alias for mdbook-lint operations
258pub type Result<T> = std::result::Result<T, MdBookLintError>;
259
260/// Convenience constructors for common error patterns
261impl MdBookLintError {
262    /// Create a parse error with position information
263    pub fn parse_error(line: usize, column: usize, message: impl Into<String>) -> Self {
264        Self::Parse {
265            line,
266            column,
267            message: message.into(),
268        }
269    }
270
271    /// Create a rule error with context
272    pub fn rule_error(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
273        Self::Rule {
274            rule_id: rule_id.into(),
275            message: message.into(),
276        }
277    }
278
279    /// Create a configuration error
280    pub fn config_error(message: impl Into<String>) -> Self {
281        Self::Config(message.into())
282    }
283
284    /// Create a document error
285    pub fn document_error(message: impl Into<String>) -> Self {
286        Self::Document(message.into())
287    }
288
289    /// Create a plugin error
290    pub fn plugin_error(message: impl Into<String>) -> Self {
291        Self::Plugin(message.into())
292    }
293
294    /// Create a registry error
295    pub fn registry_error(message: impl Into<String>) -> Self {
296        Self::Registry(message.into())
297    }
298}
299
300/// Convenience constructors for rule errors
301impl RuleError {
302    /// Create a "not found" error
303    pub fn not_found(rule_id: impl Into<String>) -> Self {
304        Self::NotFound {
305            rule_id: rule_id.into(),
306        }
307    }
308
309    /// Create an execution failed error
310    pub fn execution_failed(message: impl Into<String>) -> Self {
311        Self::ExecutionFailed {
312            message: message.into(),
313        }
314    }
315
316    /// Create an invalid config error
317    pub fn invalid_config(message: impl Into<String>) -> Self {
318        Self::InvalidConfig {
319            message: message.into(),
320        }
321    }
322
323    /// Create a dependency not met error
324    pub fn dependency_not_met(rule_id: impl Into<String>, dependency: impl Into<String>) -> Self {
325        Self::DependencyNotMet {
326            rule_id: rule_id.into(),
327            dependency: dependency.into(),
328        }
329    }
330
331    /// Create a registration conflict error
332    pub fn registration_conflict(rule_id: impl Into<String>) -> Self {
333        Self::RegistrationConflict {
334            rule_id: rule_id.into(),
335        }
336    }
337}
338
339/// Convenience constructors for document errors
340impl DocumentError {
341    /// Create a read failed error
342    pub fn read_failed(path: impl Into<String>) -> Self {
343        Self::ReadFailed { path: path.into() }
344    }
345
346    /// Create a parse failed error
347    pub fn parse_failed(reason: impl Into<String>) -> Self {
348        Self::ParseFailed {
349            reason: reason.into(),
350        }
351    }
352
353    /// Create a too large error
354    pub fn too_large(size: usize, max_size: usize) -> Self {
355        Self::TooLarge { size, max_size }
356    }
357
358    /// Create an invalid encoding error
359    pub fn invalid_encoding(path: impl Into<String>) -> Self {
360        Self::InvalidEncoding { path: path.into() }
361    }
362}
363
364/// Error context extension trait for adding contextual information to errors
365pub trait ErrorContext<T> {
366    /// Add rule context to an error
367    fn with_rule_context(self, rule_id: &str) -> Result<T>;
368
369    /// Add document context to an error
370    fn with_document_context(self, path: &str) -> Result<T>;
371
372    /// Add plugin context to an error
373    fn with_plugin_context(self, plugin_id: &str) -> Result<T>;
374
375    /// Add configuration context to an error
376    fn with_config_context(self, field: &str) -> Result<T>;
377}
378
379impl<T> ErrorContext<T> for std::result::Result<T, MdBookLintError> {
380    fn with_rule_context(self, rule_id: &str) -> Result<T> {
381        self.map_err(|e| match e {
382            MdBookLintError::Rule { message, .. } => MdBookLintError::Rule {
383                rule_id: rule_id.to_string(),
384                message,
385            },
386            other => other,
387        })
388    }
389
390    fn with_document_context(self, path: &str) -> Result<T> {
391        self.map_err(|e| match e {
392            MdBookLintError::Document(message) => {
393                MdBookLintError::Document(format!("{path}: {message}"))
394            }
395            other => other,
396        })
397    }
398
399    fn with_plugin_context(self, plugin_id: &str) -> Result<T> {
400        self.map_err(|e| match e {
401            MdBookLintError::Plugin(message) => {
402                MdBookLintError::Plugin(format!("{plugin_id}: {message}"))
403            }
404            other => other,
405        })
406    }
407
408    fn with_config_context(self, field: &str) -> Result<T> {
409        self.map_err(|e| match e {
410            MdBookLintError::Config(message) => {
411                MdBookLintError::Config(format!("{field}: {message}"))
412            }
413            other => other,
414        })
415    }
416}
417
418/// Extension trait for converting specialized errors to MdBookLintError
419pub trait IntoMdBookLintError<T> {
420    /// Convert into a Result<T, MdBookLintError>
421    fn into_mdbook_lint_error(self) -> Result<T>;
422}
423
424impl<T> IntoMdBookLintError<T> for std::result::Result<T, RuleError> {
425    fn into_mdbook_lint_error(self) -> Result<T> {
426        self.map_err(|e| match e {
427            RuleError::NotFound { rule_id } => {
428                MdBookLintError::rule_error(rule_id, "Rule not found")
429            }
430            RuleError::ExecutionFailed { message } => {
431                MdBookLintError::rule_error("unknown", message)
432            }
433            RuleError::InvalidConfig { message } => MdBookLintError::config_error(message),
434            RuleError::DependencyNotMet {
435                rule_id,
436                dependency,
437            } => MdBookLintError::rule_error(rule_id, format!("Dependency not met: {dependency}")),
438            RuleError::RegistrationConflict { rule_id } => {
439                MdBookLintError::registry_error(format!("Rule already exists: {rule_id}"))
440            }
441        })
442    }
443}
444
445impl<T> IntoMdBookLintError<T> for std::result::Result<T, DocumentError> {
446    fn into_mdbook_lint_error(self) -> Result<T> {
447        self.map_err(|e| match e {
448            DocumentError::ReadFailed { path } => {
449                MdBookLintError::document_error(format!("Failed to read: {path}"))
450            }
451            DocumentError::InvalidFormat => {
452                MdBookLintError::document_error("Invalid document format")
453            }
454            DocumentError::TooLarge { size, max_size } => MdBookLintError::document_error(format!(
455                "Document too large: {size} bytes (max: {max_size})"
456            )),
457            DocumentError::ParseFailed { reason } => {
458                MdBookLintError::document_error(format!("Parse failed: {reason}"))
459            }
460            DocumentError::InvalidEncoding { path } => {
461                MdBookLintError::document_error(format!("Invalid encoding: {path}"))
462            }
463        })
464    }
465}
466
467impl<T> IntoMdBookLintError<T> for std::result::Result<T, ConfigError> {
468    fn into_mdbook_lint_error(self) -> Result<T> {
469        self.map_err(|e| match e {
470            ConfigError::NotFound { path } => {
471                MdBookLintError::config_error(format!("Configuration not found: {path}"))
472            }
473            ConfigError::InvalidFormat { message } => {
474                MdBookLintError::config_error(format!("Invalid format: {message}"))
475            }
476            ConfigError::ValidationFailed { field, message } => {
477                MdBookLintError::config_error(format!("Validation failed for {field}: {message}"))
478            }
479            ConfigError::UnsupportedVersion { version, supported } => {
480                MdBookLintError::config_error(format!(
481                    "Unsupported version: {version} (supported: {supported})"
482                ))
483            }
484        })
485    }
486}
487
488impl<T> IntoMdBookLintError<T> for std::result::Result<T, PluginError> {
489    fn into_mdbook_lint_error(self) -> Result<T> {
490        self.map_err(|e| match e {
491            PluginError::NotFound { plugin_id } => {
492                MdBookLintError::plugin_error(format!("Plugin not found: {plugin_id}"))
493            }
494            PluginError::LoadFailed { plugin_id, reason } => {
495                MdBookLintError::plugin_error(format!("Failed to load {plugin_id}: {reason}"))
496            }
497            PluginError::InitializationFailed { plugin_id } => {
498                MdBookLintError::plugin_error(format!("Initialization failed: {plugin_id}"))
499            }
500            PluginError::VersionIncompatible {
501                plugin_id,
502                version,
503                required,
504            } => MdBookLintError::plugin_error(format!(
505                "Version incompatible: {plugin_id} version {version} (required: {required})"
506            )),
507        })
508    }
509}
510
511// Compatibility layer for migration from anyhow
512impl From<anyhow::Error> for MdBookLintError {
513    fn from(err: anyhow::Error) -> Self {
514        MdBookLintError::Document(err.to_string())
515    }
516}
517
518// Note: anyhow already provides From<MdBookLintError> via its blanket impl for StdError
519// So we don't need to implement From<MdBookLintError> for anyhow::Error
520
521/// Compatibility alias for the old error name
522pub type MdlntError = MdBookLintError;
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    #[test]
529    fn test_error_creation() {
530        let err = MdBookLintError::parse_error(10, 5, "Invalid syntax");
531        assert!(matches!(
532            err,
533            MdBookLintError::Parse {
534                line: 10,
535                column: 5,
536                ..
537            }
538        ));
539        assert!(err.to_string().contains("line 10, column 5"));
540    }
541
542    #[test]
543    fn test_all_error_variants() {
544        // Test Config error
545        let config_err = MdBookLintError::config_error("Invalid config");
546        assert!(matches!(config_err, MdBookLintError::Config(_)));
547
548        // Test Rule error
549        let rule_err = MdBookLintError::rule_error("MD001", "Rule failed");
550        assert!(matches!(rule_err, MdBookLintError::Rule { .. }));
551
552        // Test Plugin error
553        let plugin_err = MdBookLintError::plugin_error("Plugin failed");
554        assert!(matches!(plugin_err, MdBookLintError::Plugin(_)));
555
556        // Test Document error
557        let doc_err = MdBookLintError::document_error("Document error");
558        assert!(matches!(doc_err, MdBookLintError::Document(_)));
559
560        // Test Registry error
561        let registry_err = MdBookLintError::registry_error("Registry error");
562        assert!(matches!(registry_err, MdBookLintError::Registry(_)));
563    }
564
565    #[test]
566    fn test_rule_error_variants() {
567        let not_found = RuleError::not_found("MD999");
568        assert!(matches!(not_found, RuleError::NotFound { .. }));
569        assert!(not_found.to_string().contains("MD999"));
570
571        let exec_failed = RuleError::execution_failed("Test execution failed");
572        assert!(matches!(exec_failed, RuleError::ExecutionFailed { .. }));
573
574        let invalid_config = RuleError::invalid_config("Invalid rule config");
575        assert!(matches!(invalid_config, RuleError::InvalidConfig { .. }));
576
577        let dep_not_met = RuleError::dependency_not_met("MD001", "MD002");
578        assert!(matches!(dep_not_met, RuleError::DependencyNotMet { .. }));
579
580        let reg_conflict = RuleError::registration_conflict("MD001");
581        assert!(matches!(
582            reg_conflict,
583            RuleError::RegistrationConflict { .. }
584        ));
585    }
586
587    #[test]
588    fn test_document_error_variants() {
589        let read_failed = DocumentError::read_failed("test.md");
590        assert!(matches!(read_failed, DocumentError::ReadFailed { .. }));
591
592        let parse_failed = DocumentError::parse_failed("Parse error");
593        assert!(matches!(parse_failed, DocumentError::ParseFailed { .. }));
594
595        let too_large = DocumentError::too_large(1000, 500);
596        assert!(matches!(too_large, DocumentError::TooLarge { .. }));
597
598        let invalid_encoding = DocumentError::invalid_encoding("test.md");
599        assert!(matches!(
600            invalid_encoding,
601            DocumentError::InvalidEncoding { .. }
602        ));
603    }
604
605    #[test]
606    fn test_config_error_variants() {
607        let not_found = ConfigError::NotFound {
608            path: "config.toml".to_string(),
609        };
610        assert!(not_found.to_string().contains("config.toml"));
611
612        let invalid_format = ConfigError::InvalidFormat {
613            message: "Bad YAML".to_string(),
614        };
615        assert!(invalid_format.to_string().contains("Bad YAML"));
616
617        let validation_failed = ConfigError::ValidationFailed {
618            field: "rules".to_string(),
619            message: "Invalid rule".to_string(),
620        };
621        assert!(validation_failed.to_string().contains("rules"));
622
623        let unsupported_version = ConfigError::UnsupportedVersion {
624            version: "2.0".to_string(),
625            supported: "1.0-1.5".to_string(),
626        };
627        assert!(unsupported_version.to_string().contains("2.0"));
628    }
629
630    #[test]
631    fn test_plugin_error_variants() {
632        let not_found = PluginError::NotFound {
633            plugin_id: "test-plugin".to_string(),
634        };
635        assert!(not_found.to_string().contains("test-plugin"));
636
637        let load_failed = PluginError::LoadFailed {
638            plugin_id: "test-plugin".to_string(),
639            reason: "Missing file".to_string(),
640        };
641        assert!(load_failed.to_string().contains("Missing file"));
642
643        let init_failed = PluginError::InitializationFailed {
644            plugin_id: "test-plugin".to_string(),
645        };
646        assert!(init_failed.to_string().contains("test-plugin"));
647
648        let version_incompatible = PluginError::VersionIncompatible {
649            plugin_id: "test-plugin".to_string(),
650            version: "2.0".to_string(),
651            required: "1.0".to_string(),
652        };
653        assert!(version_incompatible.to_string().contains("2.0"));
654    }
655
656    #[test]
657    fn test_error_context() {
658        let result: Result<()> = Err(MdBookLintError::document_error("Something went wrong"));
659        let with_context = result.with_document_context("test.md");
660
661        assert!(with_context.is_err());
662        assert!(with_context.unwrap_err().to_string().contains("test.md"));
663    }
664
665    #[test]
666    fn test_all_error_contexts() {
667        let base_err = MdBookLintError::document_error("Base error");
668
669        let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
670        let with_rule = result.with_rule_context("MD001");
671        assert!(with_rule.is_err());
672
673        let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
674        let with_doc = result.with_document_context("test.md");
675        assert!(with_doc.is_err());
676
677        let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
678        let with_plugin = result.with_plugin_context("test-plugin");
679        assert!(with_plugin.is_err());
680
681        let result: Result<()> = Err(base_err);
682        let with_config = result.with_config_context("config.toml");
683        assert!(with_config.is_err());
684    }
685
686    #[test]
687    fn test_rule_error_conversion() {
688        let rule_err = RuleError::not_found("MD001");
689        let result: std::result::Result<(), _> = Err(rule_err);
690        let result = result.into_mdbook_lint_error();
691
692        assert!(result.is_err());
693        assert!(result.unwrap_err().to_string().contains("MD001"));
694    }
695
696    #[test]
697    fn test_all_error_conversions() {
698        // Document error conversion
699        let doc_err = DocumentError::parse_failed("Parse failed");
700        let result: std::result::Result<(), _> = Err(doc_err);
701        let converted = result.into_mdbook_lint_error();
702        assert!(converted.is_err());
703        assert!(matches!(
704            converted.unwrap_err(),
705            MdBookLintError::Document(_)
706        ));
707
708        // Config error conversion
709        let config_err = ConfigError::InvalidFormat {
710            message: "Bad format".to_string(),
711        };
712        let result: std::result::Result<(), _> = Err(config_err);
713        let converted = result.into_mdbook_lint_error();
714        assert!(converted.is_err());
715        assert!(matches!(converted.unwrap_err(), MdBookLintError::Config(_)));
716
717        // Plugin error conversion
718        let plugin_err = PluginError::NotFound {
719            plugin_id: "missing".to_string(),
720        };
721        let result: std::result::Result<(), _> = Err(plugin_err);
722        let converted = result.into_mdbook_lint_error();
723        assert!(converted.is_err());
724        assert!(matches!(converted.unwrap_err(), MdBookLintError::Plugin(_)));
725    }
726
727    #[test]
728    fn test_anyhow_compatibility() {
729        let anyhow_err = anyhow::anyhow!("Test error");
730        let mdbook_lint_err: MdBookLintError = anyhow_err.into();
731        // anyhow provides blanket From<E> impl for types implementing std::error::Error
732        let back_to_anyhow = anyhow::Error::from(mdbook_lint_err);
733
734        assert!(back_to_anyhow.to_string().contains("Test error"));
735    }
736
737    #[test]
738    fn test_io_error_conversion() {
739        use std::io::{Error, ErrorKind};
740
741        let io_err = Error::new(ErrorKind::NotFound, "File not found");
742        let mdbook_lint_err: MdBookLintError = io_err.into();
743
744        assert!(matches!(mdbook_lint_err, MdBookLintError::Io(_)));
745        assert!(mdbook_lint_err.to_string().contains("File not found"));
746    }
747
748    #[test]
749    fn test_error_chain_context() {
750        // Test chaining multiple contexts
751        let base_err = MdBookLintError::parse_error(1, 1, "Parse error");
752        let result: Result<()> = Err(base_err);
753
754        let chained: Result<()> = result.with_document_context("test.md");
755
756        assert!(chained.is_err());
757        let error_string = chained.unwrap_err().to_string();
758        assert!(
759            error_string.contains("Parse error"),
760            "Error should contain original message"
761        );
762    }
763
764    #[test]
765    fn test_error_source_chain() {
766        use std::error::Error;
767
768        let inner_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
769        let mdbook_err: MdBookLintError = inner_err.into();
770
771        // Test that the error source chain works
772        assert!(mdbook_err.source().is_some());
773        assert!(
774            mdbook_err
775                .source()
776                .unwrap()
777                .to_string()
778                .contains("File not found")
779        );
780    }
781
782    #[test]
783    fn test_mdlnt_error_alias() {
784        // Test that the MdlntError alias works
785        let _err: MdlntError = MdBookLintError::document_error("Test");
786        // This is mainly a compile-time test to ensure the alias exists
787    }
788}