Skip to main content

sphinx_ultra/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5#[allow(dead_code)]
6pub enum BuildError {
7    #[error("IO error: {0}")]
8    Io(#[from] std::io::Error),
9
10    #[error("JSON serialization error: {0}")]
11    Json(#[from] serde_json::Error),
12
13    #[error("YAML serialization error: {0}")]
14    Yaml(#[from] serde_yaml::Error),
15
16    #[error("Template rendering error: {0}")]
17    Template(String),
18
19    #[error("File parsing error: {file}: {message}")]
20    Parse { file: String, message: String },
21
22    #[error("Cache error: {0}")]
23    Cache(String),
24
25    #[error("Configuration error: {0}")]
26    Config(String),
27
28    #[error("Thread pool error: {0}")]
29    ThreadPool(#[from] rayon::ThreadPoolBuildError),
30
31    #[error("File not found: {0}")]
32    FileNotFound(String),
33
34    #[error("Invalid document format: {0}")]
35    InvalidFormat(String),
36
37    #[error("Cross-reference error: {reference} not found")]
38    CrossReference { reference: String },
39
40    #[error("Template not found: {0}")]
41    TemplateNotFound(String),
42
43    #[error("Syntax highlighting error: {0}")]
44    SyntaxHighlight(String),
45
46    #[error("Validation error: {0}")]
47    ValidationError(String),
48}
49
50#[derive(Debug, Clone)]
51pub struct BuildWarning {
52    pub file: PathBuf,
53    pub line: Option<usize>,
54    pub message: String,
55    #[allow(dead_code)]
56    pub warning_type: WarningType,
57    /// Sphinx's `type.subtype` warning category (`toc.not_readable`,
58    /// `toc.not_included`, ...), which `show_warning_types` — on by default
59    /// since Sphinx 8.3 — appends to the rendered message as ` [category]`.
60    ///
61    /// `None` for warnings Sphinx logs without a `type` (its
62    /// `SphinxLoggerAdapter` only appends the suffix when `type` is set, so
63    /// a `subtype`-only warning such as the toctree `empty_glob` one prints
64    /// bare). See `util/logging.py:545-549`.
65    pub category: Option<String>,
66}
67
68#[derive(Debug, Clone)]
69pub struct BuildErrorReport {
70    pub file: PathBuf,
71    pub line: Option<usize>,
72    pub message: String,
73    #[allow(dead_code)]
74    pub error_type: ErrorType,
75}
76
77#[derive(Debug, Clone)]
78#[allow(dead_code)]
79pub enum WarningType {
80    MissingToctreeRef,
81    OrphanedDocument,
82    BrokenCrossReference,
83    MissingFile,
84    UnusedLabel,
85    DuplicateLabel,
86    EmptyToctree,
87    Other,
88}
89
90#[derive(Debug, Clone)]
91#[allow(dead_code)]
92pub enum ErrorType {
93    ParseError,
94    FileNotFound,
95    TemplateError,
96    SyntaxError,
97    Other,
98}
99
100impl BuildWarning {
101    pub fn new(
102        file: PathBuf,
103        line: Option<usize>,
104        message: String,
105        warning_type: WarningType,
106    ) -> Self {
107        Self {
108            file,
109            line,
110            message,
111            warning_type,
112            category: None,
113        }
114    }
115
116    /// Attach Sphinx's `type.subtype` category (see [`BuildWarning::category`]).
117    #[must_use]
118    pub fn with_category(mut self, category: Option<String>) -> Self {
119        self.category = category;
120        self
121    }
122
123    /// The warning as `sphinx-build` prints it:
124    /// `path[:line]: WARNING: message[ [type.subtype]]`.
125    ///
126    /// One renderer for every sink (stderr, `-w` warning file, the
127    /// environment-oracle differential) so a message can only ever be
128    /// formatted one way.
129    ///
130    /// An empty `file` is a warning Sphinx logs with no `location` at all
131    /// (the intersphinx "failed to reach any of the inventories" report, for
132    /// one): those print as a bare `WARNING: ...`, with no location prefix
133    /// and no stray colon.
134    pub fn render(&self) -> String {
135        let category = match &self.category {
136            Some(category) => format!(" [{category}]"),
137            None => String::new(),
138        };
139        if self.file.as_os_str().is_empty() {
140            return format!("WARNING: {}{category}", self.message);
141        }
142        let line = match self.line {
143            Some(line) => format!(":{line}"),
144            None => String::new(),
145        };
146        format!(
147            "{}{line}: WARNING: {}{category}",
148            self.file.display(),
149            self.message
150        )
151    }
152
153    #[allow(dead_code)]
154    pub fn broken_cross_reference(file: PathBuf, line: Option<usize>, reference: &str) -> Self {
155        Self::new(
156            file,
157            line,
158            format!("cross-reference target not found: '{}'", reference),
159            WarningType::BrokenCrossReference,
160        )
161    }
162}
163
164impl BuildErrorReport {
165    pub fn new(file: PathBuf, line: Option<usize>, message: String, error_type: ErrorType) -> Self {
166        Self {
167            file,
168            line,
169            message,
170            error_type,
171        }
172    }
173}