Skip to main content

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 - `![alt](src)` 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}