dmc_diagnostic/lib.rs
1//! User-facing walkthrough: ../../dmc-docs/dmc-diagnostic/
2//! Run `cargo doc --open -p dmc-diagnostic` for the inline rustdoc.
3
4//! Unified diagnostic codes for the dmc pipeline.
5//!
6//! Every layer (lexer, parser, transform, codegen) emits into one shared
7//! `DiagnosticEngine<Code>`. Per-layer variants are gated behind cargo
8//! features so a crate that only needs lexer codes can opt out of the rest.
9//!
10//! ## Feature flags
11//! - `lexer` - `E***` lexer-emitted variants
12//! - `parser` - `P***` / `PW***` parser-emitted variants
13//! - `transform` - `T***` / `TW***` transform-emitted variants
14//! - `codegen` - `G***` / `GW***` codegen-emitted variants
15//!
16//! A normal full build (e.g. via `dmc-core`) enables all features.
17
18use duck_diagnostic::{Diagnostic, DiagnosticCode, Severity};
19use serde::{Deserialize, Serialize};
20
21pub mod metadata;
22
23/// Canonical fallible-return type across the dmc pipeline.
24///
25/// `DiagResult<T>` = `Result<T, Diagnostic<Code>>`. Replaces the
26/// `Result<_, std::io::Error>` / `Result<_, String>` / `Result<(), ()>`
27/// patterns scattered across the workspace so every error path lands
28/// in the same shape: a typed `Code`, a human message, optional
29/// labels / help. Callers handle errors uniformly via `?`,
30/// `engine.emit(d)`, or both.
31///
32/// Default `T = ()` for the common "did this side-effect succeed?"
33/// signature.
34///
35/// Cost: type alias only. Zero runtime overhead vs. a hand-written
36/// `Result<T, Diagnostic<Code>>`. Identical layout in monomorphised
37/// code (same machine code, same drop semantics).
38///
39/// Convention to avoid double-emit:
40/// - functions that PRODUCE a diagnostic and want the caller to
41/// decide its fate return `DiagResult<T>`.
42/// - functions that handle errors locally + emit into a passed-in
43/// `&mut DiagnosticEngine<Code>` return plain `Result<T, ()>`
44/// (or no result at all).
45///
46/// Mix the two and you get the same diagnostic in the engine twice.
47pub type DiagResult<T = ()> = Result<T, Diagnostic<Code>>;
48
49/// Stable, machine-readable diagnostic identifiers spanning the whole
50/// pipeline. Codes use disjoint string namespaces per layer:
51///
52/// - `E***` - lexer errors (feature `lexer`)
53/// - `W***` - lexer warnings (feature `lexer`)
54/// - `P***` - parser errors (feature `parser`)
55/// - `PW***` - parser warnings (feature `parser`)
56/// - `T***` - transform errors (feature `transform`)
57/// - `TW***` - transform warnings (feature `transform`)
58/// - `G***` - codegen errors (feature `codegen`)
59/// - `GW***` - codegen warnings (feature `codegen`)
60/// - `C***` - core / engine errors (feature `core`)
61/// - `CW***` - core / engine warnings (feature `core`)
62/// - `S***` - shared cross-cutting errors (IO, JSON, locks; always available)
63/// - `SW***` - shared cross-cutting warnings (always available)
64///
65/// `Custom { code, severity }` is the escape hatch for third-party
66/// transformers that want to emit through the same engine without forking
67/// this enum.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub enum Code {
70 // ===================================================================
71 // Lexer - feature = "lexer"
72 // ===================================================================
73 /// E001 - Source byte the dispatcher cannot map to any token rule.
74 #[cfg(feature = "lexer")]
75 InvalidCharacter,
76 /// E002 - Frontmatter `---` opened but inner YAML is malformed.
77 #[cfg(feature = "lexer")]
78 InvalidFrontMatter,
79 /// E003 - Quoted string literal opened without a closer before EOL/EOF.
80 #[cfg(feature = "lexer")]
81 UnterminatedString,
82 /// E004 - `{ ... }` expression opened but brace depth never returned to zero.
83 #[cfg(feature = "lexer")]
84 UnterminatedExpression,
85 /// E005 - EOF reached mid-construct where more input was required.
86 #[cfg(feature = "lexer")]
87 UnexpectedEof,
88 /// E006 - `<Tag /` seen but the closing `>` is missing.
89 #[cfg(feature = "lexer")]
90 InvalidJsxSelfClosingTag,
91 /// E007 - `<Tag ...` open tag never reached `>` or `/>` before a hard break/EOF.
92 #[cfg(feature = "lexer")]
93 UnterminatedJsxTag,
94 /// E008 - `</Tag` close tag malformed: missing name or `>`.
95 #[cfg(feature = "lexer")]
96 InvalidJsxClosingTag,
97 /// E009 - JSX attribute `name=` had no following value (string / `{expr}`).
98 #[cfg(feature = "lexer")]
99 InvalidJsxAttribute,
100 /// E010 - Fenced code block opened without an equal-length closer before EOF.
101 #[cfg(feature = "lexer")]
102 UnterminatedCodeBlock,
103
104 /// W001 - Frontmatter parsed cleanly but YAML body was empty.
105 #[cfg(feature = "lexer")]
106 EmptyFrontMatter,
107
108 // ===================================================================
109 // Parser - feature = "parser"
110 // ===================================================================
111 /// P001 - `[text](href)` opened but `]` never seen before a hard break/EOF.
112 #[cfg(feature = "parser")]
113 UnterminatedLink,
114 /// P002 - `` opened but `]` never seen before a hard break/EOF.
115 #[cfg(feature = "parser")]
116 UnterminatedImage,
117 /// P003 - Backtick run inline never closes on the same line.
118 #[cfg(feature = "parser")]
119 UnterminatedInlineCode,
120 /// P004 - Fenced code block opened but matching ` ``` ` (or longer) never seen.
121 #[cfg(feature = "parser")]
122 UnterminatedCodeBlockBlock,
123 /// P005 - `<Tag ...` opened but no `>` / `/>` before the next block break.
124 #[cfg(feature = "parser")]
125 UnterminatedJsxOpenTag,
126 /// P006 - `</Tag` opened but no `>` before the next block break.
127 #[cfg(feature = "parser")]
128 UnterminatedJsxCloseTag,
129 /// P007 - `{ ... }` expression opened but no closing `}` at matching depth.
130 #[cfg(feature = "parser")]
131 UnterminatedJsxExpression,
132 /// P008 - `{/* ... */}` markdown comment opened but no `*/}` before EOF.
133 #[cfg(feature = "parser")]
134 UnterminatedMdComment,
135 /// P009 - Frontmatter `---` opened but no closing `---` line found.
136 #[cfg(feature = "parser")]
137 UnterminatedFrontmatter,
138 /// P010 - `<Foo>` close-tag name does not match the most recent open tag.
139 #[cfg(feature = "parser")]
140 MismatchedJsxCloseTag,
141 /// P011 - Table header line had N cells but alignment row had M (M != N).
142 #[cfg(feature = "parser")]
143 TableShapeMismatch,
144 /// P012 - Setext underline `===` / `---` appeared without a preceding paragraph.
145 #[cfg(feature = "parser")]
146 StraySetextUnderline,
147 /// P013 - JSX attribute appeared with `=` but no value (string / `{expr}`).
148 #[cfg(feature = "parser")]
149 MissingJsxAttributeValue,
150 /// P014 - List item used an ordered marker number that overflows `u32`.
151 #[cfg(feature = "parser")]
152 ListMarkerOverflow,
153
154 /// PW001 - Frontmatter parsed but YAML content was empty.
155 #[cfg(feature = "parser")]
156 EmptyFrontmatter,
157 /// PW002 - YAML in frontmatter failed to parse; recovered by treating as null.
158 #[cfg(feature = "parser")]
159 InvalidFrontmatterYaml,
160 /// PW003 - Heading level > 6 was clamped to 6.
161 #[cfg(feature = "parser")]
162 HeadingLevelClamped,
163 /// PW004 - Auto-recovery synthesised a self-close for `<Tag ...` to keep parsing.
164 #[cfg(feature = "parser")]
165 RecoveredUnterminatedJsx,
166
167 // ===================================================================
168 // Transform - feature = "transform"
169 // ===================================================================
170 /// T001 - `CodeImport`: `file=path` referenced a path that could not be read.
171 #[cfg(feature = "transform")]
172 ImportFileNotFound,
173 /// T002 - `CodeImport`: `{ranges}` spec was malformed.
174 #[cfg(feature = "transform")]
175 InvalidLineRange,
176 /// T003 - `ComponentPreview`: `registry_index` JSON file failed to read.
177 #[cfg(feature = "transform")]
178 RegistryIndexUnreadable,
179 /// T004 - `ComponentPreview`: `registry_index` content was not valid JSON.
180 #[cfg(feature = "transform")]
181 RegistryIndexMalformed,
182 /// T005 - `ComponentPreview`: requested `name` not found in the registry index.
183 #[cfg(feature = "transform")]
184 RegistryEntryNotFound,
185 /// T006 - `ComponentPreview`: registry entry's first file path could not be read.
186 #[cfg(feature = "transform")]
187 RegistrySourceUnreadable,
188 /// T007 - `ComponentSource`: `path=` attribute pointed to an unreadable file.
189 #[cfg(feature = "transform")]
190 ComponentSourceUnreadable,
191 /// T008 - `CopyLinkedFiles`: write to `assets_dir` failed mid-publish.
192 #[cfg(feature = "transform")]
193 AssetCopyFailed,
194 /// T009 - `Mermaid`: `mmdc` exited non-zero or produced no SVG.
195 #[cfg(feature = "transform")]
196 MermaidRenderFailed,
197
198 /// TW001 - `Mermaid`: `mmdc` CLI is not on PATH; the transformer becomes a no-op.
199 #[cfg(feature = "transform")]
200 MmdcUnavailable,
201 /// TW002 - `ComponentPreview` / `ComponentSource`: required `name` / `path` attribute is missing.
202 #[cfg(feature = "transform")]
203 MissingComponentAttr,
204 /// TW003 - `CopyLinkedFiles`: a referenced asset path did not exist; original `src` / `href` preserved.
205 #[cfg(feature = "transform")]
206 AssetSourceMissing,
207 /// TW004 - `CodeImport` / `ComponentSource`: non-disk source (`Origin::Stdin` /
208 /// `Inline` / `Memory`) without an explicit `base_dir`, so relative `file=` /
209 /// `path=` paths can't be resolved.
210 #[cfg(feature = "transform")]
211 BaseDirNotFound,
212
213 /// TW006 - `Math` (KaTeX): `katex::Opts::builder().build()` failed; the
214 /// resulting renderer falls back to a no-op rendering for the affected
215 /// span. Almost always a sign of a broken build (the args are constants).
216 #[cfg(feature = "transform")]
217 KatexOpts,
218 /// TW005 - `PrettyCode`: a configured theme name is not present in the
219 /// bundled syntect themes. Highlight falls back to the first bundled theme,
220 /// so the missing mode silently produces wrong colors. The diagnostic
221 /// lists every bundled theme so consumers can pick a valid one.
222 #[cfg(feature = "transform")]
223 ThemeNotBundled,
224
225 // ===================================================================
226 // Codegen - feature = "codegen"
227 // ===================================================================
228 /// G001 - Codegen encountered a JSX tag with an empty / invalid name.
229 #[cfg(feature = "codegen")]
230 MalformedJsxTagName,
231
232 /// GW001 - `MdxBodyEmitter`: GFM `Table` node dropped (no inline table renderer
233 /// yet). Run `disable-gfm` first to convert tables to plain text.
234 #[cfg(feature = "codegen")]
235 MdxTableUnsupported,
236 /// GW002 - `HtmlEmitter`: raw `JsxExpression` discarded (HTML output can't run JS);
237 /// use the MDX body emitter for full JSX support.
238 #[cfg(feature = "codegen")]
239 HtmlExpressionDropped,
240
241 // ===================================================================
242 // Core - feature = "core"
243 //
244 // Engine-level codes use the `C***` / `CW***` namespace so they
245 // never collide with the lexer's `E***` / `W***` strings (Cargo
246 // unifies features across the workspace; defaults enable every
247 // layer at once, so namespaces have to be globally unique).
248 // ===================================================================
249 /// C001 - No root dir configured.
250 #[cfg(feature = "core")]
251 NoRootDir,
252 /// C002 - No config file found.
253 #[cfg(feature = "core")]
254 NoConfig,
255 /// C003 - No collections configured.
256 #[cfg(feature = "core")]
257 NoCollections,
258 /// C004 - Collection not found.
259 #[cfg(feature = "core")]
260 CollectionNotFound,
261 /// C005 - Collection pattern not found.
262 #[cfg(feature = "core")]
263 CollectionPatternNotFound,
264 /// C006 - Collection schema not found.
265 #[cfg(feature = "core")]
266 CollectionSchemaNotFound,
267 /// C007 - Invalid config.
268 #[cfg(feature = "core")]
269 InvalidConfig,
270 /// C008 - Invalid config path.
271 #[cfg(feature = "core")]
272 InvalidConfigPath,
273 /// CW001 - Config file already exists at the target path.
274 #[cfg(feature = "core")]
275 ConfigExists,
276
277 // ===================================================================
278 // Shared (no feature gate)
279 //
280 // Cross-cutting concerns every layer hits: filesystem IO, JSON
281 // round-trip, mutex poisoning. NOT gated behind a per-layer feature
282 // because any crate may produce these (math cache load/save, sidecar
283 // dispatch, engine output write, registry-index parse, ...). Using
284 // them avoids leaking layer-specific codes (e.g. `EmptyFrontMatter`)
285 // into IO failures unrelated to frontmatter.
286 //
287 // Namespace: `S***` for errors, `SW***` for warnings.
288 // ===================================================================
289 /// S001 - `std::fs::read*` / `read_to_string` failed at the named path.
290 IoRead,
291 /// S002 - `std::fs::write` failed at the named path.
292 IoWrite,
293 /// S003 - `std::fs::create_dir_all` failed for the named path.
294 IoCreateDir,
295 /// S004 - `serde_json` (or other deserializer) failed to parse the input.
296 JsonDeserialize,
297 /// S005 - `serde_json` (or other serializer) failed to encode the value.
298 JsonSerialize,
299 /// S006 - A `Mutex` / `RwLock` was poisoned by a panic in another thread.
300 LockPoisoned,
301 /// SW001 - Best-effort recoverable IO miss (e.g. cache load fell through).
302 /// Build continues without the cached state.
303 IoRecoverable,
304
305 // ===================================================================
306 // User-defined escape hatch - always available
307 // ===================================================================
308 /// Carry an arbitrary code string + explicit severity through the same
309 /// engine. For third-party transformer authors who don't want to fork this
310 /// enum. Prefer adding a typed variant when contributing upstream.
311 Custom { code: String, severity: Severity },
312}
313
314impl DiagnosticCode for Code {
315 fn code(&self) -> &str {
316 match self {
317 // Lexer
318 #[cfg(feature = "lexer")]
319 Self::InvalidCharacter => "E001",
320 #[cfg(feature = "lexer")]
321 Self::InvalidFrontMatter => "E002",
322 #[cfg(feature = "lexer")]
323 Self::UnterminatedString => "E003",
324 #[cfg(feature = "lexer")]
325 Self::UnterminatedExpression => "E004",
326 #[cfg(feature = "lexer")]
327 Self::UnexpectedEof => "E005",
328 #[cfg(feature = "lexer")]
329 Self::InvalidJsxSelfClosingTag => "E006",
330 #[cfg(feature = "lexer")]
331 Self::UnterminatedJsxTag => "E007",
332 #[cfg(feature = "lexer")]
333 Self::InvalidJsxClosingTag => "E008",
334 #[cfg(feature = "lexer")]
335 Self::InvalidJsxAttribute => "E009",
336 #[cfg(feature = "lexer")]
337 Self::UnterminatedCodeBlock => "E010",
338 #[cfg(feature = "lexer")]
339 Self::EmptyFrontMatter => "W001",
340
341 // Parser
342 #[cfg(feature = "parser")]
343 Self::UnterminatedLink => "P001",
344 #[cfg(feature = "parser")]
345 Self::UnterminatedImage => "P002",
346 #[cfg(feature = "parser")]
347 Self::UnterminatedInlineCode => "P003",
348 #[cfg(feature = "parser")]
349 Self::UnterminatedCodeBlockBlock => "P004",
350 #[cfg(feature = "parser")]
351 Self::UnterminatedJsxOpenTag => "P005",
352 #[cfg(feature = "parser")]
353 Self::UnterminatedJsxCloseTag => "P006",
354 #[cfg(feature = "parser")]
355 Self::UnterminatedJsxExpression => "P007",
356 #[cfg(feature = "parser")]
357 Self::UnterminatedMdComment => "P008",
358 #[cfg(feature = "parser")]
359 Self::UnterminatedFrontmatter => "P009",
360 #[cfg(feature = "parser")]
361 Self::MismatchedJsxCloseTag => "P010",
362 #[cfg(feature = "parser")]
363 Self::TableShapeMismatch => "P011",
364 #[cfg(feature = "parser")]
365 Self::StraySetextUnderline => "P012",
366 #[cfg(feature = "parser")]
367 Self::MissingJsxAttributeValue => "P013",
368 #[cfg(feature = "parser")]
369 Self::ListMarkerOverflow => "P014",
370 #[cfg(feature = "parser")]
371 Self::EmptyFrontmatter => "PW001",
372 #[cfg(feature = "parser")]
373 Self::InvalidFrontmatterYaml => "PW002",
374 #[cfg(feature = "parser")]
375 Self::HeadingLevelClamped => "PW003",
376 #[cfg(feature = "parser")]
377 Self::RecoveredUnterminatedJsx => "PW004",
378
379 // Transform
380 #[cfg(feature = "transform")]
381 Self::ImportFileNotFound => "T001",
382 #[cfg(feature = "transform")]
383 Self::InvalidLineRange => "T002",
384 #[cfg(feature = "transform")]
385 Self::RegistryIndexUnreadable => "T003",
386 #[cfg(feature = "transform")]
387 Self::RegistryIndexMalformed => "T004",
388 #[cfg(feature = "transform")]
389 Self::RegistryEntryNotFound => "T005",
390 #[cfg(feature = "transform")]
391 Self::RegistrySourceUnreadable => "T006",
392 #[cfg(feature = "transform")]
393 Self::ComponentSourceUnreadable => "T007",
394 #[cfg(feature = "transform")]
395 Self::AssetCopyFailed => "T008",
396 #[cfg(feature = "transform")]
397 Self::MermaidRenderFailed => "T009",
398 #[cfg(feature = "transform")]
399 Self::MmdcUnavailable => "TW001",
400 #[cfg(feature = "transform")]
401 Self::MissingComponentAttr => "TW002",
402 #[cfg(feature = "transform")]
403 Self::AssetSourceMissing => "TW003",
404 #[cfg(feature = "transform")]
405 Self::BaseDirNotFound => "TW004",
406 #[cfg(feature = "transform")]
407 Self::ThemeNotBundled => "TW005",
408 #[cfg(feature = "transform")]
409 Self::KatexOpts => "TW006",
410
411 #[cfg(feature = "codegen")]
412 Self::MalformedJsxTagName => "G001",
413 #[cfg(feature = "codegen")]
414 Self::MdxTableUnsupported => "GW001",
415 #[cfg(feature = "codegen")]
416 Self::HtmlExpressionDropped => "GW002",
417
418 // Core
419 #[cfg(feature = "core")]
420 Self::NoRootDir => "C001",
421 #[cfg(feature = "core")]
422 Self::NoConfig => "C002",
423 #[cfg(feature = "core")]
424 Self::NoCollections => "C003",
425 #[cfg(feature = "core")]
426 Self::CollectionNotFound => "C004",
427 #[cfg(feature = "core")]
428 Self::CollectionPatternNotFound => "C005",
429 #[cfg(feature = "core")]
430 Self::CollectionSchemaNotFound => "C006",
431 #[cfg(feature = "core")]
432 Self::InvalidConfig => "C007",
433 #[cfg(feature = "core")]
434 Self::InvalidConfigPath => "C008",
435 #[cfg(feature = "core")]
436 Self::ConfigExists => "CW001",
437
438 // Shared
439 Self::IoRead => "S001",
440 Self::IoWrite => "S002",
441 Self::IoCreateDir => "S003",
442 Self::JsonDeserialize => "S004",
443 Self::JsonSerialize => "S005",
444 Self::LockPoisoned => "S006",
445 Self::IoRecoverable => "SW001",
446
447 Self::Custom { code, .. } => code.as_str(),
448 }
449 }
450
451 fn severity(&self) -> Severity {
452 match self {
453 // Lexer errors
454 #[cfg(feature = "lexer")]
455 Self::InvalidCharacter
456 | Self::InvalidFrontMatter
457 | Self::UnterminatedString
458 | Self::UnterminatedExpression
459 | Self::UnexpectedEof
460 | Self::InvalidJsxSelfClosingTag
461 | Self::UnterminatedJsxTag
462 | Self::InvalidJsxClosingTag
463 | Self::InvalidJsxAttribute
464 | Self::UnterminatedCodeBlock => Severity::Error,
465 #[cfg(feature = "lexer")]
466 Self::EmptyFrontMatter => Severity::Warning,
467
468 // Parser errors
469 #[cfg(feature = "parser")]
470 Self::UnterminatedLink
471 | Self::UnterminatedImage
472 | Self::UnterminatedInlineCode
473 | Self::UnterminatedCodeBlockBlock
474 | Self::UnterminatedJsxOpenTag
475 | Self::UnterminatedJsxCloseTag
476 | Self::UnterminatedJsxExpression
477 | Self::UnterminatedMdComment
478 | Self::UnterminatedFrontmatter
479 | Self::MismatchedJsxCloseTag
480 | Self::TableShapeMismatch
481 | Self::StraySetextUnderline
482 | Self::MissingJsxAttributeValue
483 | Self::ListMarkerOverflow => Severity::Error,
484 #[cfg(feature = "parser")]
485 Self::EmptyFrontmatter
486 | Self::InvalidFrontmatterYaml
487 | Self::HeadingLevelClamped
488 | Self::RecoveredUnterminatedJsx => Severity::Warning,
489
490 // Transform errors
491 #[cfg(feature = "transform")]
492 Self::ImportFileNotFound
493 | Self::InvalidLineRange
494 | Self::RegistryIndexUnreadable
495 | Self::RegistryIndexMalformed
496 | Self::RegistryEntryNotFound
497 | Self::RegistrySourceUnreadable
498 | Self::ComponentSourceUnreadable
499 | Self::AssetCopyFailed
500 | Self::MermaidRenderFailed => Severity::Error,
501 #[cfg(feature = "transform")]
502 Self::MmdcUnavailable
503 | Self::MissingComponentAttr
504 | Self::AssetSourceMissing
505 | Self::BaseDirNotFound
506 | Self::ThemeNotBundled
507 | Self::KatexOpts => Severity::Warning,
508
509 #[cfg(feature = "codegen")]
510 Self::MalformedJsxTagName => Severity::Error,
511 #[cfg(feature = "codegen")]
512 Self::MdxTableUnsupported | Self::HtmlExpressionDropped => Severity::Warning,
513
514 // Core errors / warnings
515 #[cfg(feature = "core")]
516 Self::NoRootDir
517 | Self::NoConfig
518 | Self::NoCollections
519 | Self::CollectionNotFound
520 | Self::CollectionPatternNotFound
521 | Self::CollectionSchemaNotFound
522 | Self::InvalidConfig
523 | Self::InvalidConfigPath => Severity::Error,
524 #[cfg(feature = "core")]
525 Self::ConfigExists => Severity::Warning,
526
527 // Shared
528 Self::IoRead
529 | Self::IoWrite
530 | Self::IoCreateDir
531 | Self::JsonDeserialize
532 | Self::JsonSerialize
533 | Self::LockPoisoned => Severity::Error,
534 Self::IoRecoverable => Severity::Warning,
535
536 Self::Custom { severity, .. } => *severity,
537 }
538 }
539}