Skip to main content

dmc_diagnostic/
lib.rs

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