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
130  /// PW001 - Frontmatter parsed but YAML content was empty.
131  #[cfg(feature = "parser")]
132  EmptyFrontmatter,
133  /// PW002 - YAML in frontmatter failed to parse; recovered by treating as null.
134  #[cfg(feature = "parser")]
135  InvalidFrontmatterYaml,
136  /// PW003 - Heading level > 6 was clamped to 6.
137  #[cfg(feature = "parser")]
138  HeadingLevelClamped,
139  /// PW004 - Auto-recovery synthesised a self-close for `<Tag ...` to keep parsing.
140  #[cfg(feature = "parser")]
141  RecoveredUnterminatedJsx,
142
143  // Transform (feature = "transform")
144  /// T001 - `CodeImport`: `file=path` referenced a path that could not be read.
145  #[cfg(feature = "transform")]
146  ImportFileNotFound,
147  /// T002 - `CodeImport`: `{ranges}` spec was malformed.
148  #[cfg(feature = "transform")]
149  InvalidLineRange,
150  /// T003 - `ComponentPreview`: `registry_index` JSON file failed to read.
151  #[cfg(feature = "transform")]
152  RegistryIndexUnreadable,
153  /// T004 - `ComponentPreview`: `registry_index` content was not valid JSON.
154  #[cfg(feature = "transform")]
155  RegistryIndexMalformed,
156  /// T005 - `ComponentPreview`: requested `name` not found in the registry index.
157  #[cfg(feature = "transform")]
158  RegistryEntryNotFound,
159  /// T006 - `ComponentPreview`: registry entry's first file path could not be read.
160  #[cfg(feature = "transform")]
161  RegistrySourceUnreadable,
162  /// T007 - `ComponentSource`: `path=` attribute pointed to an unreadable file.
163  #[cfg(feature = "transform")]
164  ComponentSourceUnreadable,
165  /// T008 - `CopyLinkedFiles`: write to `assets_dir` failed mid-publish.
166  #[cfg(feature = "transform")]
167  AssetCopyFailed,
168  /// T009 - `Mermaid`: `mmdc` exited non-zero or produced no SVG.
169  #[cfg(feature = "transform")]
170  MermaidRenderFailed,
171
172  /// TW001 - `Mermaid`: `mmdc` CLI is not on PATH; the transformer becomes a no-op.
173  #[cfg(feature = "transform")]
174  MmdcUnavailable,
175  /// TW002 - `ComponentPreview` / `ComponentSource`: required `name` / `path` attribute is missing.
176  #[cfg(feature = "transform")]
177  MissingComponentAttr,
178  /// TW003 - `CopyLinkedFiles`: a referenced asset path did not exist; original `src` / `href` preserved.
179  #[cfg(feature = "transform")]
180  AssetSourceMissing,
181  /// TW004 - `CodeImport` / `ComponentSource`: non-disk source (`Origin::Stdin` /
182  /// `Inline` / `Memory`) without an explicit `base_dir`, so relative `file=` /
183  /// `path=` paths can't be resolved.
184  #[cfg(feature = "transform")]
185  BaseDirNotFound,
186
187  /// TW006 - `Math` (KaTeX): `katex::Opts::builder().build()` failed; the
188  /// resulting renderer falls back to a no-op rendering for the affected
189  /// span. Almost always a sign of a broken build (the args are constants).
190  #[cfg(feature = "transform")]
191  KatexOpts,
192  /// TW005 - `PrettyCode`: a configured theme name is not present in the
193  /// bundled syntect themes. Highlight falls back to the first bundled theme,
194  /// so the missing mode silently produces wrong colors. The diagnostic
195  /// lists every bundled theme so consumers can pick a valid one.
196  #[cfg(feature = "transform")]
197  ThemeNotBundled,
198
199  // Codegen (feature = "codegen")
200  /// G001 - Codegen encountered a JSX tag with an empty / invalid name.
201  #[cfg(feature = "codegen")]
202  MalformedJsxTagName,
203
204  /// GW001 - `MdxBodyEmitter`: GFM `Table` node dropped (no inline table renderer
205  /// yet). Run `disable-gfm` first to convert tables to plain text.
206  #[cfg(feature = "codegen")]
207  MdxTableUnsupported,
208  /// GW002 - `HtmlEmitter`: raw `JsxExpression` discarded (HTML output can't run JS);
209  /// use the MDX body emitter for full JSX support.
210  #[cfg(feature = "codegen")]
211  HtmlExpressionDropped,
212
213  // Core (feature = "core").
214  // `C***`/`CW***` namespace avoids collision with lexer `E***`/`W***`:
215  // Cargo unifies features workspace-wide, so namespaces must be globally unique.
216  /// C001 - No root dir configured.
217  #[cfg(feature = "core")]
218  NoRootDir,
219  /// C002 - No config file found.
220  #[cfg(feature = "core")]
221  NoConfig,
222  /// C003 - No collections configured.
223  #[cfg(feature = "core")]
224  NoCollections,
225  /// C004 - Collection not found.
226  #[cfg(feature = "core")]
227  CollectionNotFound,
228  /// C005 - Collection pattern not found.
229  #[cfg(feature = "core")]
230  CollectionPatternNotFound,
231  /// C006 - Collection schema not found.
232  #[cfg(feature = "core")]
233  CollectionSchemaNotFound,
234  /// C007 - Invalid config.
235  #[cfg(feature = "core")]
236  InvalidConfig,
237  /// C008 - Invalid config path.
238  #[cfg(feature = "core")]
239  InvalidConfigPath,
240  /// CW001 - Config file already exists at the target path.
241  #[cfg(feature = "core")]
242  ConfigExists,
243
244  // Shared (always available; `S***` / `SW***`).
245  // Cross-cutting IO / JSON / lock concerns. Ungated because any crate may
246  // produce these; gating would force callers to reuse a layer-specific code
247  // (e.g. `EmptyFrontMatter`) for unrelated IO failures.
248  /// S001 - `std::fs::read*` / `read_to_string` failed at the named path.
249  IoRead,
250  /// S002 - `std::fs::write` failed at the named path.
251  IoWrite,
252  /// S003 - `std::fs::create_dir_all` failed for the named path.
253  IoCreateDir,
254  /// S004 - `serde_json` (or other deserializer) failed to parse the input.
255  JsonDeserialize,
256  /// S005 - `serde_json` (or other serializer) failed to encode the value.
257  JsonSerialize,
258  /// S006 - A `Mutex` / `RwLock` was poisoned by a panic in another thread.
259  LockPoisoned,
260  /// SW001 - Best-effort recoverable IO miss (e.g. cache load fell through).
261  /// Build continues without the cached state.
262  IoRecoverable,
263
264  /// Third-party escape hatch carrying an arbitrary code + severity.
265  /// Prefer adding a typed variant when contributing upstream.
266  Custom { code: String, severity: Severity },
267}
268
269impl DiagnosticCode for Code {
270  fn code(&self) -> &str {
271    match self {
272      // Lexer
273      #[cfg(feature = "lexer")]
274      Self::InvalidCharacter => "E001",
275      #[cfg(feature = "lexer")]
276      Self::InvalidFrontMatter => "E002",
277      #[cfg(feature = "lexer")]
278      Self::UnterminatedString => "E003",
279      #[cfg(feature = "lexer")]
280      Self::UnterminatedExpression => "E004",
281      #[cfg(feature = "lexer")]
282      Self::UnexpectedEof => "E005",
283      #[cfg(feature = "lexer")]
284      Self::InvalidJsxSelfClosingTag => "E006",
285      #[cfg(feature = "lexer")]
286      Self::UnterminatedJsxTag => "E007",
287      #[cfg(feature = "lexer")]
288      Self::InvalidJsxClosingTag => "E008",
289      #[cfg(feature = "lexer")]
290      Self::InvalidJsxAttribute => "E009",
291      #[cfg(feature = "lexer")]
292      Self::UnterminatedCodeBlock => "E010",
293      #[cfg(feature = "lexer")]
294      Self::EmptyFrontMatter => "W001",
295
296      // Parser
297      #[cfg(feature = "parser")]
298      Self::UnterminatedLink => "P001",
299      #[cfg(feature = "parser")]
300      Self::UnterminatedImage => "P002",
301      #[cfg(feature = "parser")]
302      Self::UnterminatedInlineCode => "P003",
303      #[cfg(feature = "parser")]
304      Self::UnterminatedCodeBlockBlock => "P004",
305      #[cfg(feature = "parser")]
306      Self::UnterminatedJsxOpenTag => "P005",
307      #[cfg(feature = "parser")]
308      Self::UnterminatedJsxCloseTag => "P006",
309      #[cfg(feature = "parser")]
310      Self::UnterminatedJsxExpression => "P007",
311      #[cfg(feature = "parser")]
312      Self::UnterminatedMdComment => "P008",
313      #[cfg(feature = "parser")]
314      Self::UnterminatedFrontmatter => "P009",
315      #[cfg(feature = "parser")]
316      Self::MismatchedJsxCloseTag => "P010",
317      #[cfg(feature = "parser")]
318      Self::TableShapeMismatch => "P011",
319      #[cfg(feature = "parser")]
320      Self::StraySetextUnderline => "P012",
321      #[cfg(feature = "parser")]
322      Self::MissingJsxAttributeValue => "P013",
323      #[cfg(feature = "parser")]
324      Self::ListMarkerOverflow => "P014",
325      #[cfg(feature = "parser")]
326      Self::EmptyFrontmatter => "PW001",
327      #[cfg(feature = "parser")]
328      Self::InvalidFrontmatterYaml => "PW002",
329      #[cfg(feature = "parser")]
330      Self::HeadingLevelClamped => "PW003",
331      #[cfg(feature = "parser")]
332      Self::RecoveredUnterminatedJsx => "PW004",
333
334      // Transform
335      #[cfg(feature = "transform")]
336      Self::ImportFileNotFound => "T001",
337      #[cfg(feature = "transform")]
338      Self::InvalidLineRange => "T002",
339      #[cfg(feature = "transform")]
340      Self::RegistryIndexUnreadable => "T003",
341      #[cfg(feature = "transform")]
342      Self::RegistryIndexMalformed => "T004",
343      #[cfg(feature = "transform")]
344      Self::RegistryEntryNotFound => "T005",
345      #[cfg(feature = "transform")]
346      Self::RegistrySourceUnreadable => "T006",
347      #[cfg(feature = "transform")]
348      Self::ComponentSourceUnreadable => "T007",
349      #[cfg(feature = "transform")]
350      Self::AssetCopyFailed => "T008",
351      #[cfg(feature = "transform")]
352      Self::MermaidRenderFailed => "T009",
353      #[cfg(feature = "transform")]
354      Self::MmdcUnavailable => "TW001",
355      #[cfg(feature = "transform")]
356      Self::MissingComponentAttr => "TW002",
357      #[cfg(feature = "transform")]
358      Self::AssetSourceMissing => "TW003",
359      #[cfg(feature = "transform")]
360      Self::BaseDirNotFound => "TW004",
361      #[cfg(feature = "transform")]
362      Self::ThemeNotBundled => "TW005",
363      #[cfg(feature = "transform")]
364      Self::KatexOpts => "TW006",
365
366      #[cfg(feature = "codegen")]
367      Self::MalformedJsxTagName => "G001",
368      #[cfg(feature = "codegen")]
369      Self::MdxTableUnsupported => "GW001",
370      #[cfg(feature = "codegen")]
371      Self::HtmlExpressionDropped => "GW002",
372
373      // Core
374      #[cfg(feature = "core")]
375      Self::NoRootDir => "C001",
376      #[cfg(feature = "core")]
377      Self::NoConfig => "C002",
378      #[cfg(feature = "core")]
379      Self::NoCollections => "C003",
380      #[cfg(feature = "core")]
381      Self::CollectionNotFound => "C004",
382      #[cfg(feature = "core")]
383      Self::CollectionPatternNotFound => "C005",
384      #[cfg(feature = "core")]
385      Self::CollectionSchemaNotFound => "C006",
386      #[cfg(feature = "core")]
387      Self::InvalidConfig => "C007",
388      #[cfg(feature = "core")]
389      Self::InvalidConfigPath => "C008",
390      #[cfg(feature = "core")]
391      Self::ConfigExists => "CW001",
392
393      // Shared
394      Self::IoRead => "S001",
395      Self::IoWrite => "S002",
396      Self::IoCreateDir => "S003",
397      Self::JsonDeserialize => "S004",
398      Self::JsonSerialize => "S005",
399      Self::LockPoisoned => "S006",
400      Self::IoRecoverable => "SW001",
401
402      Self::Custom { code, .. } => code.as_str(),
403    }
404  }
405
406  fn severity(&self) -> Severity {
407    match self {
408      // Lexer errors
409      #[cfg(feature = "lexer")]
410      Self::InvalidCharacter
411      | Self::InvalidFrontMatter
412      | Self::UnterminatedString
413      | Self::UnterminatedExpression
414      | Self::UnexpectedEof
415      | Self::InvalidJsxSelfClosingTag
416      | Self::UnterminatedJsxTag
417      | Self::InvalidJsxClosingTag
418      | Self::InvalidJsxAttribute
419      | Self::UnterminatedCodeBlock => Severity::Error,
420      #[cfg(feature = "lexer")]
421      Self::EmptyFrontMatter => Severity::Warning,
422
423      // Parser errors
424      #[cfg(feature = "parser")]
425      Self::UnterminatedLink
426      | Self::UnterminatedImage
427      | Self::UnterminatedInlineCode
428      | Self::UnterminatedCodeBlockBlock
429      | Self::UnterminatedJsxOpenTag
430      | Self::UnterminatedJsxCloseTag
431      | Self::UnterminatedJsxExpression
432      | Self::UnterminatedMdComment
433      | Self::UnterminatedFrontmatter
434      | Self::MismatchedJsxCloseTag
435      | Self::TableShapeMismatch
436      | Self::StraySetextUnderline
437      | Self::MissingJsxAttributeValue
438      | Self::ListMarkerOverflow => Severity::Error,
439      #[cfg(feature = "parser")]
440      Self::EmptyFrontmatter
441      | Self::InvalidFrontmatterYaml
442      | Self::HeadingLevelClamped
443      | Self::RecoveredUnterminatedJsx => Severity::Warning,
444
445      // Transform errors
446      #[cfg(feature = "transform")]
447      Self::ImportFileNotFound
448      | Self::InvalidLineRange
449      | Self::RegistryIndexUnreadable
450      | Self::RegistryIndexMalformed
451      | Self::RegistryEntryNotFound
452      | Self::RegistrySourceUnreadable
453      | Self::ComponentSourceUnreadable
454      | Self::AssetCopyFailed
455      | Self::MermaidRenderFailed => Severity::Error,
456      #[cfg(feature = "transform")]
457      Self::MmdcUnavailable
458      | Self::MissingComponentAttr
459      | Self::AssetSourceMissing
460      | Self::BaseDirNotFound
461      | Self::ThemeNotBundled
462      | Self::KatexOpts => Severity::Warning,
463
464      #[cfg(feature = "codegen")]
465      Self::MalformedJsxTagName => Severity::Error,
466      #[cfg(feature = "codegen")]
467      Self::MdxTableUnsupported | Self::HtmlExpressionDropped => Severity::Warning,
468
469      // Core errors / warnings
470      #[cfg(feature = "core")]
471      Self::NoRootDir
472      | Self::NoConfig
473      | Self::NoCollections
474      | Self::CollectionNotFound
475      | Self::CollectionPatternNotFound
476      | Self::CollectionSchemaNotFound
477      | Self::InvalidConfig
478      | Self::InvalidConfigPath => Severity::Error,
479      #[cfg(feature = "core")]
480      Self::ConfigExists => Severity::Warning,
481
482      // Shared
483      Self::IoRead
484      | Self::IoWrite
485      | Self::IoCreateDir
486      | Self::JsonDeserialize
487      | Self::JsonSerialize
488      | Self::LockPoisoned => Severity::Error,
489      Self::IoRecoverable => Severity::Warning,
490
491      Self::Custom { severity, .. } => *severity,
492    }
493  }
494}