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::{DiagnosticCode, Severity};
16use serde::{Deserialize, Serialize};
17
18pub mod metadata;
19
20/// Stable, machine-readable diagnostic identifiers spanning the whole
21/// pipeline. Codes use disjoint string namespaces per layer:
22///
23/// - `E***`  - lexer errors  (feature `lexer`)
24/// - `W***`  - lexer warnings (feature `lexer`)
25/// - `P***`  - parser errors  (feature `parser`)
26/// - `PW***` - parser warnings (feature `parser`)
27/// - `T***`  - transform errors  (feature `transform`)
28/// - `TW***` - transform warnings (feature `transform`)
29/// - `G***`  - codegen errors  (feature `codegen`)
30/// - `GW***` - codegen warnings (feature `codegen`)
31///
32/// `Custom { code, severity }` is the escape hatch for third-party
33/// transformers that want to emit through the same engine without forking
34/// this enum.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub enum Code {
37  // ===================================================================
38  // Lexer - feature = "lexer"
39  // ===================================================================
40  /// E001 - Source byte the dispatcher cannot map to any token rule.
41  #[cfg(feature = "lexer")]
42  InvalidCharacter,
43  /// E002 - Frontmatter `---` opened but inner YAML is malformed.
44  #[cfg(feature = "lexer")]
45  InvalidFrontMatter,
46  /// E003 - Quoted string literal opened without a closer before EOL/EOF.
47  #[cfg(feature = "lexer")]
48  UnterminatedString,
49  /// E004 - `{ ... }` expression opened but brace depth never returned to zero.
50  #[cfg(feature = "lexer")]
51  UnterminatedExpression,
52  /// E005 - EOF reached mid-construct where more input was required.
53  #[cfg(feature = "lexer")]
54  UnexpectedEof,
55  /// E006 - `<Tag /` seen but the closing `>` is missing.
56  #[cfg(feature = "lexer")]
57  InvalidJsxSelfClosingTag,
58  /// E007 - `<Tag ...` open tag never reached `>` or `/>` before a hard break/EOF.
59  #[cfg(feature = "lexer")]
60  UnterminatedJsxTag,
61  /// E008 - `</Tag` close tag malformed: missing name or `>`.
62  #[cfg(feature = "lexer")]
63  InvalidJsxClosingTag,
64  /// E009 - JSX attribute `name=` had no following value (string / `{expr}`).
65  #[cfg(feature = "lexer")]
66  InvalidJsxAttribute,
67  /// E010 - Fenced code block opened without an equal-length closer before EOF.
68  #[cfg(feature = "lexer")]
69  UnterminatedCodeBlock,
70
71  /// W001 - Frontmatter parsed cleanly but YAML body was empty.
72  #[cfg(feature = "lexer")]
73  EmptyFrontMatter,
74
75  // ===================================================================
76  // Parser - feature = "parser"
77  // ===================================================================
78  /// P001 - `[text](href)` opened but `]` never seen before a hard break/EOF.
79  #[cfg(feature = "parser")]
80  UnterminatedLink,
81  /// P002 - `![alt](src)` opened but `]` never seen before a hard break/EOF.
82  #[cfg(feature = "parser")]
83  UnterminatedImage,
84  /// P003 - Backtick run inline never closes on the same line.
85  #[cfg(feature = "parser")]
86  UnterminatedInlineCode,
87  /// P004 - Fenced code block opened but matching ` ``` ` (or longer) never seen.
88  #[cfg(feature = "parser")]
89  UnterminatedCodeBlockBlock,
90  /// P005 - `<Tag ...` opened but no `>` / `/>` before the next block break.
91  #[cfg(feature = "parser")]
92  UnterminatedJsxOpenTag,
93  /// P006 - `</Tag` opened but no `>` before the next block break.
94  #[cfg(feature = "parser")]
95  UnterminatedJsxCloseTag,
96  /// P007 - `{ ... }` expression opened but no closing `}` at matching depth.
97  #[cfg(feature = "parser")]
98  UnterminatedJsxExpression,
99  /// P008 - `{/* ... */}` markdown comment opened but no `*/}` before EOF.
100  #[cfg(feature = "parser")]
101  UnterminatedMdComment,
102  /// P009 - Frontmatter `---` opened but no closing `---` line found.
103  #[cfg(feature = "parser")]
104  UnterminatedFrontmatter,
105  /// P010 - `<Foo>` close-tag name does not match the most recent open tag.
106  #[cfg(feature = "parser")]
107  MismatchedJsxCloseTag,
108  /// P011 - Table header line had N cells but alignment row had M (M != N).
109  #[cfg(feature = "parser")]
110  TableShapeMismatch,
111  /// P012 - Setext underline `===` / `---` appeared without a preceding paragraph.
112  #[cfg(feature = "parser")]
113  StraySetextUnderline,
114  /// P013 - JSX attribute appeared with `=` but no value (string / `{expr}`).
115  #[cfg(feature = "parser")]
116  MissingJsxAttributeValue,
117  /// P014 - List item used an ordered marker number that overflows `u32`.
118  #[cfg(feature = "parser")]
119  ListMarkerOverflow,
120
121  /// PW001 - Frontmatter parsed but YAML content was empty.
122  #[cfg(feature = "parser")]
123  EmptyFrontmatter,
124  /// PW002 - YAML in frontmatter failed to parse; recovered by treating as null.
125  #[cfg(feature = "parser")]
126  InvalidFrontmatterYaml,
127  /// PW003 - Heading level > 6 was clamped to 6.
128  #[cfg(feature = "parser")]
129  HeadingLevelClamped,
130  /// PW004 - Auto-recovery synthesised a self-close for `<Tag ...` to keep parsing.
131  #[cfg(feature = "parser")]
132  RecoveredUnterminatedJsx,
133
134  // ===================================================================
135  // Transform - feature = "transform"
136  // ===================================================================
137  /// T001 - `CodeImport`: `file=path` referenced a path that could not be read.
138  #[cfg(feature = "transform")]
139  ImportFileNotFound,
140  /// T002 - `CodeImport`: `{ranges}` spec was malformed.
141  #[cfg(feature = "transform")]
142  InvalidLineRange,
143  /// T003 - `ComponentPreview`: `registry_index` JSON file failed to read.
144  #[cfg(feature = "transform")]
145  RegistryIndexUnreadable,
146  /// T004 - `ComponentPreview`: `registry_index` content was not valid JSON.
147  #[cfg(feature = "transform")]
148  RegistryIndexMalformed,
149  /// T005 - `ComponentPreview`: requested `name` not found in the registry index.
150  #[cfg(feature = "transform")]
151  RegistryEntryNotFound,
152  /// T006 - `ComponentPreview`: registry entry's first file path could not be read.
153  #[cfg(feature = "transform")]
154  RegistrySourceUnreadable,
155  /// T007 - `ComponentSource`: `path=` attribute pointed to an unreadable file.
156  #[cfg(feature = "transform")]
157  ComponentSourceUnreadable,
158  /// T008 - `CopyLinkedFiles`: write to `assets_dir` failed mid-publish.
159  #[cfg(feature = "transform")]
160  AssetCopyFailed,
161  /// T009 - `Mermaid`: `mmdc` exited non-zero or produced no SVG.
162  #[cfg(feature = "transform")]
163  MermaidRenderFailed,
164
165  /// TW001 - `Mermaid`: `mmdc` CLI is not on PATH; the transformer becomes a no-op.
166  #[cfg(feature = "transform")]
167  MmdcUnavailable,
168  /// TW002 - `ComponentPreview` / `ComponentSource`: required `name` / `path` attribute is missing.
169  #[cfg(feature = "transform")]
170  MissingComponentAttr,
171  /// TW003 - `CopyLinkedFiles`: a referenced asset path did not exist; original `src` / `href` preserved.
172  #[cfg(feature = "transform")]
173  AssetSourceMissing,
174  /// TW004 - `CodeImport` / `ComponentSource`: non-disk source (`Origin::Stdin` /
175  /// `Inline` / `Memory`) without an explicit `base_dir`, so relative `file=` /
176  /// `path=` paths can't be resolved.
177  #[cfg(feature = "transform")]
178  BaseDirNotFound,
179
180  // ===================================================================
181  // Codegen - feature = "codegen"
182  // ===================================================================
183  /// G001 - Codegen encountered a JSX tag with an empty / invalid name.
184  #[cfg(feature = "codegen")]
185  MalformedJsxTagName,
186
187  /// GW001 - `MdxBodyEmitter`: GFM `Table` node dropped (no inline table renderer
188  /// yet). Run `disable-gfm` first to convert tables to plain text.
189  #[cfg(feature = "codegen")]
190  MdxTableUnsupported,
191  /// GW002 - `HtmlEmitter`: raw `JsxExpression` discarded (HTML output can't run JS);
192  /// use the MDX body emitter for full JSX support.
193  #[cfg(feature = "codegen")]
194  HtmlExpressionDropped,
195
196  // ===================================================================
197  // User-defined escape hatch - always available
198  // ===================================================================
199  /// Carry an arbitrary code string + explicit severity through the same
200  /// engine. For third-party transformer authors who don't want to fork this
201  /// enum. Prefer adding a typed variant when contributing upstream.
202  Custom { code: String, severity: Severity },
203}
204
205impl DiagnosticCode for Code {
206  fn code(&self) -> &str {
207    match self {
208      // Lexer
209      #[cfg(feature = "lexer")]
210      Self::InvalidCharacter => "E001",
211      #[cfg(feature = "lexer")]
212      Self::InvalidFrontMatter => "E002",
213      #[cfg(feature = "lexer")]
214      Self::UnterminatedString => "E003",
215      #[cfg(feature = "lexer")]
216      Self::UnterminatedExpression => "E004",
217      #[cfg(feature = "lexer")]
218      Self::UnexpectedEof => "E005",
219      #[cfg(feature = "lexer")]
220      Self::InvalidJsxSelfClosingTag => "E006",
221      #[cfg(feature = "lexer")]
222      Self::UnterminatedJsxTag => "E007",
223      #[cfg(feature = "lexer")]
224      Self::InvalidJsxClosingTag => "E008",
225      #[cfg(feature = "lexer")]
226      Self::InvalidJsxAttribute => "E009",
227      #[cfg(feature = "lexer")]
228      Self::UnterminatedCodeBlock => "E010",
229      #[cfg(feature = "lexer")]
230      Self::EmptyFrontMatter => "W001",
231
232      // Parser
233      #[cfg(feature = "parser")]
234      Self::UnterminatedLink => "P001",
235      #[cfg(feature = "parser")]
236      Self::UnterminatedImage => "P002",
237      #[cfg(feature = "parser")]
238      Self::UnterminatedInlineCode => "P003",
239      #[cfg(feature = "parser")]
240      Self::UnterminatedCodeBlockBlock => "P004",
241      #[cfg(feature = "parser")]
242      Self::UnterminatedJsxOpenTag => "P005",
243      #[cfg(feature = "parser")]
244      Self::UnterminatedJsxCloseTag => "P006",
245      #[cfg(feature = "parser")]
246      Self::UnterminatedJsxExpression => "P007",
247      #[cfg(feature = "parser")]
248      Self::UnterminatedMdComment => "P008",
249      #[cfg(feature = "parser")]
250      Self::UnterminatedFrontmatter => "P009",
251      #[cfg(feature = "parser")]
252      Self::MismatchedJsxCloseTag => "P010",
253      #[cfg(feature = "parser")]
254      Self::TableShapeMismatch => "P011",
255      #[cfg(feature = "parser")]
256      Self::StraySetextUnderline => "P012",
257      #[cfg(feature = "parser")]
258      Self::MissingJsxAttributeValue => "P013",
259      #[cfg(feature = "parser")]
260      Self::ListMarkerOverflow => "P014",
261      #[cfg(feature = "parser")]
262      Self::EmptyFrontmatter => "PW001",
263      #[cfg(feature = "parser")]
264      Self::InvalidFrontmatterYaml => "PW002",
265      #[cfg(feature = "parser")]
266      Self::HeadingLevelClamped => "PW003",
267      #[cfg(feature = "parser")]
268      Self::RecoveredUnterminatedJsx => "PW004",
269
270      // Transform
271      #[cfg(feature = "transform")]
272      Self::ImportFileNotFound => "T001",
273      #[cfg(feature = "transform")]
274      Self::InvalidLineRange => "T002",
275      #[cfg(feature = "transform")]
276      Self::RegistryIndexUnreadable => "T003",
277      #[cfg(feature = "transform")]
278      Self::RegistryIndexMalformed => "T004",
279      #[cfg(feature = "transform")]
280      Self::RegistryEntryNotFound => "T005",
281      #[cfg(feature = "transform")]
282      Self::RegistrySourceUnreadable => "T006",
283      #[cfg(feature = "transform")]
284      Self::ComponentSourceUnreadable => "T007",
285      #[cfg(feature = "transform")]
286      Self::AssetCopyFailed => "T008",
287      #[cfg(feature = "transform")]
288      Self::MermaidRenderFailed => "T009",
289      #[cfg(feature = "transform")]
290      Self::MmdcUnavailable => "TW001",
291      #[cfg(feature = "transform")]
292      Self::MissingComponentAttr => "TW002",
293      #[cfg(feature = "transform")]
294      Self::AssetSourceMissing => "TW003",
295      #[cfg(feature = "transform")]
296      Self::BaseDirNotFound => "TW004",
297
298      #[cfg(feature = "codegen")]
299      Self::MalformedJsxTagName => "G001",
300      #[cfg(feature = "codegen")]
301      Self::MdxTableUnsupported => "GW001",
302      #[cfg(feature = "codegen")]
303      Self::HtmlExpressionDropped => "GW002",
304
305      Self::Custom { code, .. } => code.as_str(),
306    }
307  }
308
309  fn severity(&self) -> Severity {
310    match self {
311      // Lexer errors
312      #[cfg(feature = "lexer")]
313      Self::InvalidCharacter
314      | Self::InvalidFrontMatter
315      | Self::UnterminatedString
316      | Self::UnterminatedExpression
317      | Self::UnexpectedEof
318      | Self::InvalidJsxSelfClosingTag
319      | Self::UnterminatedJsxTag
320      | Self::InvalidJsxClosingTag
321      | Self::InvalidJsxAttribute
322      | Self::UnterminatedCodeBlock => Severity::Error,
323      #[cfg(feature = "lexer")]
324      Self::EmptyFrontMatter => Severity::Warning,
325
326      // Parser errors
327      #[cfg(feature = "parser")]
328      Self::UnterminatedLink
329      | Self::UnterminatedImage
330      | Self::UnterminatedInlineCode
331      | Self::UnterminatedCodeBlockBlock
332      | Self::UnterminatedJsxOpenTag
333      | Self::UnterminatedJsxCloseTag
334      | Self::UnterminatedJsxExpression
335      | Self::UnterminatedMdComment
336      | Self::UnterminatedFrontmatter
337      | Self::MismatchedJsxCloseTag
338      | Self::TableShapeMismatch
339      | Self::StraySetextUnderline
340      | Self::MissingJsxAttributeValue
341      | Self::ListMarkerOverflow => Severity::Error,
342      #[cfg(feature = "parser")]
343      Self::EmptyFrontmatter
344      | Self::InvalidFrontmatterYaml
345      | Self::HeadingLevelClamped
346      | Self::RecoveredUnterminatedJsx => Severity::Warning,
347
348      // Transform errors
349      #[cfg(feature = "transform")]
350      Self::ImportFileNotFound
351      | Self::InvalidLineRange
352      | Self::RegistryIndexUnreadable
353      | Self::RegistryIndexMalformed
354      | Self::RegistryEntryNotFound
355      | Self::RegistrySourceUnreadable
356      | Self::ComponentSourceUnreadable
357      | Self::AssetCopyFailed
358      | Self::MermaidRenderFailed => Severity::Error,
359      #[cfg(feature = "transform")]
360      Self::MmdcUnavailable | Self::MissingComponentAttr | Self::AssetSourceMissing | Self::BaseDirNotFound => {
361        Severity::Warning
362      },
363
364      #[cfg(feature = "codegen")]
365      Self::MalformedJsxTagName => Severity::Error,
366      #[cfg(feature = "codegen")]
367      Self::MdxTableUnsupported | Self::HtmlExpressionDropped => Severity::Warning,
368
369      Self::Custom { severity, .. } => *severity,
370    }
371  }
372}