Skip to main content

asciidoc_parser/
warnings.rs

1//! Describes conditions where a parse result might be unexpected.
2//!
3//! Every UTF-8 string is a valid AsciiDoc document, so parsing never fails.
4//! Anything ambiguous or likely unintended is reported as a [`Warning`]
5//! instead, and a caller is advised to review the warnings a parse produced
6//! (see [`Document::warnings`](crate::Document::warnings)).
7
8use thiserror::Error;
9
10use crate::{Span, parser::SourceLine};
11
12/// Describes a possible parse error (i.e. a "warning") and its location.
13///
14/// In `asciidoc-parser`, all documents are parseable, so this mechanism is used
15/// to convey conditions where the parse result might be unexpected.
16#[derive(Clone, Debug, Eq, PartialEq)]
17#[non_exhaustive]
18pub struct Warning<'src> {
19    /// Location where the warning was detected.
20    pub source: Span<'src>,
21
22    /// Type of warning detected.
23    pub warning: WarningType,
24
25    /// A pre-resolved originating `(file, line)` for this warning, independent
26    /// of the document source map.
27    ///
28    /// This is `None` for the overwhelming majority of warnings: their
29    /// [`source`](Self::source) span indexes the (preprocessed) document
30    /// source, so the originating file and line are recovered by resolving
31    /// `source.line()` through [`Document::source_map`].
32    ///
33    /// It is `Some` only when the warning arises from content that was expanded
34    /// *privately* and never appears in the document source – an `include::`
35    /// directive buried inside an owned (include-expanded) AsciiDoc table cell.
36    /// No document span maps to such a directive, so its true `(file, line)` is
37    /// resolved when the warning is raised (against the owning cell's own
38    /// source map) and carried here directly. In that case `source` still
39    /// points at the enclosing cell's directive line in the document (a
40    /// best-effort anchor), but `origin` names where the failing directive
41    /// actually lives.
42    ///
43    /// [`Document::source_map`]: crate::Document::source_map
44    pub origin: Option<SourceLine>,
45}
46
47/// Type of possible parse error that was detected.
48///
49/// This enum is `non_exhaustive`: new conditions are recognized as the parser
50/// grows, so a host matching on it needs a catch-all arm.
51#[derive(Clone, Eq, Error, Hash, PartialEq)]
52#[non_exhaustive]
53pub enum WarningType {
54    /// A quoted attribute value ran to the end of its line (or the end of the
55    /// attribute list) without a matching closing quote.
56    #[error("an attribute value is missing its terminating quote")]
57    AttributeValueMissingTerminatingQuote,
58
59    /// A document header was not followed by a blank line, so the line that
60    /// follows it can not be parsed as part of the header.
61    #[error(
62        "document header wasn't terminated by a blank line (this line can't be parsed as part of a document header)"
63    )]
64    DocumentHeaderNotTerminated,
65
66    /// The `inline` doctype was requested for a document that holds no single
67    /// paragraph, verbatim, or raw block to convert.
68    #[error(
69        "no inline candidate; use the inline doctype to convert a single paragraph, verbatim, or raw block"
70    )]
71    NoInlineDoctypeCandidate,
72
73    /// An element attribute was written with a name and `=` but no value.
74    #[error("an empty attribute value was detected")]
75    EmptyAttributeValue,
76
77    /// A shorthand element attribute marker (`.` for a role, `#` for an ID, or
78    /// `%` for an option) was found with no name after it.
79    #[error(
80        "a shorthand element attribute marker ('.', '#', or '%') was found with no subsequent text"
81    )]
82    EmptyShorthandName,
83
84    /// The name in a block or inline macro is not a valid identifier.
85    #[error("macro name is not a valid identifier")]
86    InvalidMacroName,
87
88    /// A media macro (`image::`, `video::`, or `audio::`) was written without
89    /// the target that names the media to embed.
90    #[error("media macro missing target")]
91    MediaMacroMissingTarget,
92
93    /// A macro was written without the `[…]` attribute list that terminates it.
94    #[error("macro missing attribute list")]
95    MacroMissingAttributeList,
96
97    /// A block macro was written without the `::` that separates its name from
98    /// its target.
99    #[error("macro missing :: separator")]
100    MacroMissingSeparator,
101
102    /// A quoted attribute value in an attribute list was followed by something
103    /// other than the comma that separates it from the next attribute.
104    #[error("missing comma after quoted attribute value")]
105    MissingCommaAfterQuotedAttributeValue,
106
107    /// A delimited block was opened but the matching closing delimiter was
108    /// never found, so the block runs to the end of the document.
109    #[error("closing marker for delimited block not found")]
110    UnterminatedDelimitedBlock,
111
112    /// A block title (`.Title`) or attribute list (`[…]`) was found at the end
113    /// of the document or immediately before a blank line, with no block for it
114    /// to describe.
115    #[error("a block title or attribute list was found without a subsequent block")]
116    MissingBlockAfterTitleOrAttributeList,
117
118    /// A block anchor (`[[…]]`) was written with no name between its brackets.
119    #[error("block anchor name is empty")]
120    EmptyBlockAnchorName,
121
122    /// A block anchor (`[[…]]`) names an ID containing characters that are not
123    /// permitted in a name.
124    #[error("block anchor name contains invalid name characters")]
125    InvalidBlockAnchorName,
126
127    /// The document tried to set an attribute that the API caller locked when
128    /// it configured the parser. The field is the attribute name.
129    #[error("attribute {0:?} can not be modified by document")]
130    AttributeValueIsLocked(String),
131
132    /// An ID was assigned to an element when an earlier element had already
133    /// registered it. The field is the duplicated ID.
134    #[error("duplicate ID: {0:?} is already registered")]
135    DuplicateId(String),
136
137    /// A level-0 section heading (`= Title`) was found somewhere other than the
138    /// document header, where this crate does not support it.
139    #[error("level 0 section headings not supported")]
140    Level0SectionHeadingNotSupported,
141
142    /// A section heading skipped one or more levels below its parent. The
143    /// fields are the expected level and the level actually found.
144    #[error("section heading level skipped (expected {0}, found {1})")]
145    SectionHeadingLevelSkipped(usize, usize),
146
147    /// A section heading nests deeper than the deepest supported level. The
148    /// field is the level found.
149    #[error("section heading level exceeds maximum (maximum 5, found {0})")]
150    SectionHeadingLevelExceedsMaximum(usize),
151
152    /// A `leveloffset` shifted a section heading outside the supported range,
153    /// so its level was clamped. The fields are the offset level and the level
154    /// it was clamped to.
155    #[error("section heading level {0} is outside the supported range 1-5; clamped to {1}")]
156    SectionHeadingLevelOutOfRange(i32, usize),
157
158    /// A `leveloffset` is so large (or so negative) that no authored heading
159    /// level could land inside the supported range. The field is the offset.
160    #[error("leveloffset {0} places every section heading outside the supported range 1-5")]
161    LeveloffsetExcludesAllHeadingLevels(i32),
162
163    /// A special section that does not support nested sections (a `glossary`,
164    /// `bibliography`, `colophon`, `dedication`, or `index` section) contains a
165    /// subsection. The field is the section's style name. Mirrors Asciidoctor,
166    /// which permits subsections only within the `appendix`, `preface`, and
167    /// `abstract` special sections.
168    #[error("{0} sections do not support nested sections")]
169    SpecialSectionCannotHaveNestedSections(String),
170
171    /// An explicitly-numbered list item does not continue the sequence its list
172    /// established. The fields are the expected and actual indexes.
173    #[error("list item index: expected {0}, got {1}")]
174    ListItemOutOfSequence(String, String),
175
176    /// A callout list item has no matching callout marker in the verbatim block
177    /// it annotates. The field is the callout number.
178    #[error("no callout found for <{0}>")]
179    NoCalloutFound(usize),
180
181    /// A callout list item does not continue the sequence its list established.
182    /// The fields are the expected and actual indexes.
183    #[error("callout list item index: expected {0}, got {1}")]
184    CalloutListItemOutOfSequence(usize, usize),
185
186    /// A table row holds more cells than the table's column count allows; the
187    /// surplus cell is dropped.
188    #[error("dropping table cell because it exceeds the specified number of columns")]
189    TableCellExceedsColumnCount,
190
191    /// A quoted field in a CSV-format table was never closed; the cell is set
192    /// to empty.
193    #[error("unclosed quote in CSV data; setting cell to empty")]
194    TableCsvDataHasUnclosedQuote,
195
196    /// A table row does not begin with the cell separator its table uses;
197    /// parsing recovers by assuming one.
198    #[error("table is missing a leading separator; recovering automatically")]
199    TableMissingLeadingSeparator,
200
201    /// A table ended part-way through a row; the cells of that partial row are
202    /// dropped.
203    #[error("dropping cells from incomplete row; detected end of table")]
204    TableIncompleteRowAtEndOfTable,
205
206    /// An attribute reference (`{name}`) names an attribute that is not set,
207    /// under `attribute-missing=warn`. The field is the attribute name.
208    #[error("skipping reference to missing attribute: {0}")]
209    SkippingReferenceToMissingAttribute(String),
210
211    /// A `stem:` macro named a substitution type that is not recognized. The
212    /// field is the unrecognized name.
213    #[error("invalid substitution type for stem macro: {0}")]
214    InvalidSubstitutionTypeForStemMacro(String),
215
216    /// A passthrough macro (`pass:`) named a substitution type that is not
217    /// recognized. The field is the unrecognized name.
218    #[error("invalid substitution type for passthrough macro: {0}")]
219    InvalidSubstitutionTypeForPassthroughMacro(String),
220
221    /// One or more unrecognized substitution names in a block's `subs`
222    /// attribute. The names are joined with `", "`; any recognized names in
223    /// the same list are still honored.
224    #[error("invalid substitution type for block: {0}")]
225    InvalidSubstitutionTypeForBlock(String),
226
227    /// A footnote reference (`footnote:id[]`) names an ID that was never
228    /// defined by an earlier footnote.
229    #[error("invalid footnote reference: {0}")]
230    InvalidFootnoteReference(String),
231
232    /// The deprecated `footnoteref:[…]` macro was used outside compatibility
233    /// mode. The footnote macro with a target should be used instead.
234    #[error("found deprecated footnoteref macro: {0}; use footnote macro with target instead")]
235    DeprecatedFootnoterefMacro(String),
236
237    /// An `include::` directive named a file that the configured include file
238    /// handler could not resolve. The field is the target as written.
239    #[error("include file not found: {0}")]
240    IncludeFileNotFound(String),
241
242    /// An `include::` directive named a file that exists but the configured
243    /// include file handler could not read (for example a permission or other
244    /// IO error). The field is the target as written. Asciidoctor distinguishes
245    /// this from [`IncludeFileNotFound`](Self::IncludeFileNotFound), logging
246    /// `include file not readable` rather than `include file not found`.
247    #[error("include file not readable: {0}")]
248    IncludeFileNotReadable(String),
249
250    /// An `include::` directive named a file that the configured include file
251    /// handler found and read but which is not valid UTF-8, and which the
252    /// handler could not transcode (no `encoding` attribute, or one it does not
253    /// support). The field is the target as written. Asciidoctor treats this as
254    /// a fatal error (`invalid byte sequence in UTF-8`) that aborts the
255    /// conversion; this crate favors recoverable warnings, so it drops the
256    /// include and records this warning instead. This is distinct from
257    /// [`NonUtf8IncludeEncoding`](Self::NonUtf8IncludeEncoding), which reports
258    /// an unsupported `encoding` request for content the handler *did* return.
259    #[error("include file not decodable (invalid byte sequence in UTF-8): {0}")]
260    IncludeFileNotDecodable(String),
261
262    /// An include directive's target referenced a missing attribute while
263    /// `attribute-missing` was set to `warn`, so the directive was dropped
264    /// without being resolved. (Under `drop-line` the directive line is
265    /// removed silently instead.) The field is the directive as written.
266    #[error("include dropped due to missing attribute: {0}")]
267    IncludeDroppedDueToMissingAttribute(String),
268
269    /// An include directive was not expanded because the file containing it
270    /// already sits at the maximum include depth (the `max-include-depth`
271    /// attribute, possibly lowered by an enclosing include directive's `depth`
272    /// attribute). The field is the relative maximum in effect – the number of
273    /// levels that were permitted below the file that established the limit –
274    /// matching the number Asciidoctor reports.
275    #[error("maximum include depth of {0} exceeded")]
276    MaxIncludeDepthExceeded(usize),
277
278    /// Block parsing reached the maximum nesting depth (the `max-block-nesting`
279    /// attribute, default 32, API-only) before the innermost content was
280    /// parsed, so the over-nested content was truncated rather than descended
281    /// into. This bounds native recursion – a delimited block's body, a section
282    /// body, a table cell, or a nested list each parse on a fresh call stack –
283    /// so a crafted document cannot overflow the stack and abort the process.
284    /// The field is the limit in effect.
285    #[error("maximum block nesting depth of {0} exceeded")]
286    MaxBlockNestingExceeded(usize),
287
288    /// An include directive specified an `encoding` attribute whose value is
289    /// not UTF-8. The parser only handles UTF-8 content, so the requested
290    /// encoding cannot be honored.
291    #[error("include encoding is not supported (only UTF-8 is supported): {0}")]
292    NonUtf8IncludeEncoding(String),
293
294    /// A conditional preprocessor directive (`ifdef`, `ifndef`, `ifeval`, or
295    /// `endif`) is malformed. The first field is the specific reason (e.g.
296    /// `missing target`, `target not permitted`, `missing expression`, `invalid
297    /// expression`, `text not permitted`); the second is the offending
298    /// directive as written.
299    #[error("malformed preprocessor directive - {0}: {1}")]
300    MalformedConditionalDirective(String, String),
301
302    /// An `endif` preprocessor directive was found with no matching open
303    /// conditional. The field is the offending directive as written.
304    #[error("unmatched preprocessor directive: {0}")]
305    UnmatchedConditionalDirective(String),
306
307    /// An `endif` preprocessor directive names a different target than the
308    /// conditional it would close. The field is the offending directive as
309    /// written.
310    #[error("mismatched preprocessor directive: {0}")]
311    MismatchedConditionalDirective(String),
312
313    /// A conditional preprocessor directive (`ifdef`, `ifndef`, or `ifeval`)
314    /// was opened but never closed by a matching `endif`. The field is the
315    /// opening directive as written.
316    #[error("detected unterminated preprocessor conditional directive: {0}")]
317    UnterminatedConditionalDirective(String),
318
319    /// One or more tags named by an include directive's `tag` / `tags`
320    /// attribute were never found in the include file. The field is the
321    /// pre-formatted, pluralized subject – `tag '<name>'` for a single missing
322    /// tag, or `tags '<name>, <name>'` (comma-joined, in the order specified)
323    /// for several.
324    #[error("{0} not found in include file")]
325    IncludeTagNotFound(String),
326
327    /// A tagged region in an include file was opened by a `tag::` directive but
328    /// never closed. The field is the unclosed tag name.
329    #[error("detected unclosed tag in include file: {0}")]
330    IncludeTagUnclosed(String),
331
332    /// An `end::` tag directive in an include file names a different tag than
333    /// the region currently open. The first field is the expected (open) tag,
334    /// the second is the tag actually found.
335    #[error("mismatched end tag in include file (expected {0} but found {1})")]
336    IncludeTagMismatchedEnd(String, String),
337
338    /// An `end::` tag directive in an include file was found with no
339    /// corresponding open region. The field is the unexpected tag name.
340    #[error("unexpected end tag in include file: {0}")]
341    IncludeTagUnexpectedEnd(String),
342
343    /// An `[abstract]` block was found as a direct child of a document without
344    /// a doctitle when the doctype is `book`. Asciidoctor excludes such a
345    /// block's content from the converted output.
346    #[error(
347        "abstract block cannot be used in a document without a doctitle when doctype is book. Excluding block content."
348    )]
349    AbstractBlockInBookWithoutDoctitle,
350
351    /// A cross-reference (`<<id>>` or `xref:id[…]`) named a target that the
352    /// resolution pass could not resolve. The reference still renders as the
353    /// unresolved fallback link (`<a href="#id">[id]</a>`). The field is the
354    /// target exactly as written in the source.
355    ///
356    /// Asciidoctor reports this only in verbose (pedantic) mode, since a
357    /// reference to an anchor that is not stored in the parse tree can be a
358    /// false positive.
359    #[error("possible invalid reference: {0}")]
360    PossibleInvalidReference(String),
361
362    /// An explicit `link:` macro named a target whose URI scheme can execute
363    /// script (`javascript:`, `data:`, or `vbscript:`). The macro is not turned
364    /// into a link; it is left as literal source text instead. The field is the
365    /// target exactly as written. This is a security measure with no
366    /// counterpart in Ruby Asciidoctor.
367    #[error("rejected link with potentially unsafe scheme (rendered as text): {0}")]
368    UnsafeLinkSchemeRejected(String),
369}
370
371impl std::fmt::Debug for WarningType {
372    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373        match self {
374            WarningType::AttributeValueMissingTerminatingQuote => {
375                write!(f, "WarningType::AttributeValueMissingTerminatingQuote")
376            }
377
378            WarningType::DocumentHeaderNotTerminated => {
379                write!(f, "WarningType::DocumentHeaderNotTerminated")
380            }
381
382            WarningType::NoInlineDoctypeCandidate => {
383                write!(f, "WarningType::NoInlineDoctypeCandidate")
384            }
385
386            WarningType::EmptyAttributeValue => write!(f, "WarningType::EmptyAttributeValue"),
387            WarningType::EmptyShorthandName => write!(f, "WarningType::EmptyShorthandName"),
388            WarningType::InvalidMacroName => write!(f, "WarningType::InvalidMacroName"),
389
390            WarningType::MediaMacroMissingTarget => {
391                write!(f, "WarningType::MediaMacroMissingTarget")
392            }
393
394            WarningType::MacroMissingAttributeList => {
395                write!(f, "WarningType::MacroMissingAttributeList")
396            }
397
398            WarningType::MacroMissingSeparator => {
399                write!(f, "WarningType::MacroMissingSeparator")
400            }
401
402            WarningType::MissingCommaAfterQuotedAttributeValue => {
403                write!(f, "WarningType::MissingCommaAfterQuotedAttributeValue")
404            }
405
406            WarningType::UnterminatedDelimitedBlock => {
407                write!(f, "WarningType::UnterminatedDelimitedBlock")
408            }
409
410            WarningType::MissingBlockAfterTitleOrAttributeList => {
411                write!(f, "WarningType::MissingBlockAfterTitleOrAttributeList")
412            }
413
414            WarningType::EmptyBlockAnchorName => write!(f, "WarningType::EmptyBlockAnchorName"),
415            WarningType::InvalidBlockAnchorName => write!(f, "WarningType::InvalidBlockAnchorName"),
416
417            WarningType::AttributeValueIsLocked(value) => f
418                .debug_tuple("WarningType::AttributeValueIsLocked")
419                .field(value)
420                .finish(),
421
422            WarningType::DuplicateId(id) => {
423                f.debug_tuple("WarningType::DuplicateId").field(id).finish()
424            }
425
426            WarningType::Level0SectionHeadingNotSupported => {
427                write!(f, "WarningType::Level0SectionHeadingNotSupported")
428            }
429
430            WarningType::SectionHeadingLevelSkipped(expected, found) => f
431                .debug_tuple("WarningType::SectionHeadingLevelSkipped")
432                .field(expected)
433                .field(found)
434                .finish(),
435
436            WarningType::SectionHeadingLevelExceedsMaximum(found) => f
437                .debug_tuple("WarningType::SectionHeadingLevelExceedsMaximum")
438                .field(found)
439                .finish(),
440
441            WarningType::SectionHeadingLevelOutOfRange(computed, clamped) => f
442                .debug_tuple("WarningType::SectionHeadingLevelOutOfRange")
443                .field(computed)
444                .field(clamped)
445                .finish(),
446
447            WarningType::LeveloffsetExcludesAllHeadingLevels(offset) => f
448                .debug_tuple("WarningType::LeveloffsetExcludesAllHeadingLevels")
449                .field(offset)
450                .finish(),
451
452            WarningType::SpecialSectionCannotHaveNestedSections(style) => f
453                .debug_tuple("WarningType::SpecialSectionCannotHaveNestedSections")
454                .field(style)
455                .finish(),
456
457            WarningType::ListItemOutOfSequence(expected, actual) => f
458                .debug_tuple("WarningType::ListItemOutOfSequence")
459                .field(expected)
460                .field(actual)
461                .finish(),
462
463            WarningType::NoCalloutFound(number) => f
464                .debug_tuple("WarningType::NoCalloutFound")
465                .field(number)
466                .finish(),
467
468            WarningType::CalloutListItemOutOfSequence(expected, actual) => f
469                .debug_tuple("WarningType::CalloutListItemOutOfSequence")
470                .field(expected)
471                .field(actual)
472                .finish(),
473
474            WarningType::TableCellExceedsColumnCount => {
475                write!(f, "WarningType::TableCellExceedsColumnCount")
476            }
477
478            WarningType::TableCsvDataHasUnclosedQuote => {
479                write!(f, "WarningType::TableCsvDataHasUnclosedQuote")
480            }
481
482            WarningType::TableMissingLeadingSeparator => {
483                write!(f, "WarningType::TableMissingLeadingSeparator")
484            }
485
486            WarningType::TableIncompleteRowAtEndOfTable => {
487                write!(f, "WarningType::TableIncompleteRowAtEndOfTable")
488            }
489
490            WarningType::SkippingReferenceToMissingAttribute(name) => f
491                .debug_tuple("WarningType::SkippingReferenceToMissingAttribute")
492                .field(name)
493                .finish(),
494
495            WarningType::InvalidSubstitutionTypeForStemMacro(subs) => f
496                .debug_tuple("WarningType::InvalidSubstitutionTypeForStemMacro")
497                .field(subs)
498                .finish(),
499
500            WarningType::InvalidSubstitutionTypeForPassthroughMacro(subs) => f
501                .debug_tuple("WarningType::InvalidSubstitutionTypeForPassthroughMacro")
502                .field(subs)
503                .finish(),
504
505            WarningType::InvalidSubstitutionTypeForBlock(subs) => f
506                .debug_tuple("WarningType::InvalidSubstitutionTypeForBlock")
507                .field(subs)
508                .finish(),
509
510            WarningType::InvalidFootnoteReference(id) => f
511                .debug_tuple("WarningType::InvalidFootnoteReference")
512                .field(id)
513                .finish(),
514
515            WarningType::DeprecatedFootnoterefMacro(macro_text) => f
516                .debug_tuple("WarningType::DeprecatedFootnoterefMacro")
517                .field(macro_text)
518                .finish(),
519
520            WarningType::IncludeFileNotFound(target) => f
521                .debug_tuple("WarningType::IncludeFileNotFound")
522                .field(target)
523                .finish(),
524
525            WarningType::IncludeFileNotReadable(target) => f
526                .debug_tuple("WarningType::IncludeFileNotReadable")
527                .field(target)
528                .finish(),
529
530            WarningType::IncludeFileNotDecodable(target) => f
531                .debug_tuple("WarningType::IncludeFileNotDecodable")
532                .field(target)
533                .finish(),
534
535            WarningType::IncludeDroppedDueToMissingAttribute(directive) => f
536                .debug_tuple("WarningType::IncludeDroppedDueToMissingAttribute")
537                .field(directive)
538                .finish(),
539
540            WarningType::MaxIncludeDepthExceeded(depth) => f
541                .debug_tuple("WarningType::MaxIncludeDepthExceeded")
542                .field(depth)
543                .finish(),
544
545            WarningType::MaxBlockNestingExceeded(depth) => f
546                .debug_tuple("WarningType::MaxBlockNestingExceeded")
547                .field(depth)
548                .finish(),
549
550            WarningType::NonUtf8IncludeEncoding(encoding) => f
551                .debug_tuple("WarningType::NonUtf8IncludeEncoding")
552                .field(encoding)
553                .finish(),
554
555            WarningType::MalformedConditionalDirective(reason, directive) => f
556                .debug_tuple("WarningType::MalformedConditionalDirective")
557                .field(reason)
558                .field(directive)
559                .finish(),
560
561            WarningType::UnmatchedConditionalDirective(directive) => f
562                .debug_tuple("WarningType::UnmatchedConditionalDirective")
563                .field(directive)
564                .finish(),
565
566            WarningType::MismatchedConditionalDirective(directive) => f
567                .debug_tuple("WarningType::MismatchedConditionalDirective")
568                .field(directive)
569                .finish(),
570
571            WarningType::UnterminatedConditionalDirective(directive) => f
572                .debug_tuple("WarningType::UnterminatedConditionalDirective")
573                .field(directive)
574                .finish(),
575
576            WarningType::IncludeTagNotFound(tag) => f
577                .debug_tuple("WarningType::IncludeTagNotFound")
578                .field(tag)
579                .finish(),
580
581            WarningType::IncludeTagUnclosed(tag) => f
582                .debug_tuple("WarningType::IncludeTagUnclosed")
583                .field(tag)
584                .finish(),
585
586            WarningType::IncludeTagMismatchedEnd(expected, found) => f
587                .debug_tuple("WarningType::IncludeTagMismatchedEnd")
588                .field(expected)
589                .field(found)
590                .finish(),
591
592            WarningType::IncludeTagUnexpectedEnd(tag) => f
593                .debug_tuple("WarningType::IncludeTagUnexpectedEnd")
594                .field(tag)
595                .finish(),
596
597            WarningType::AbstractBlockInBookWithoutDoctitle => {
598                write!(f, "WarningType::AbstractBlockInBookWithoutDoctitle")
599            }
600
601            WarningType::PossibleInvalidReference(target) => f
602                .debug_tuple("WarningType::PossibleInvalidReference")
603                .field(target)
604                .finish(),
605
606            WarningType::UnsafeLinkSchemeRejected(target) => f
607                .debug_tuple("WarningType::UnsafeLinkSchemeRejected")
608                .field(target)
609                .finish(),
610        }
611    }
612}
613
614/// Return type used to signal one or more possible parse error.
615#[derive(Clone, Debug, Eq, PartialEq)]
616pub(crate) struct MatchAndWarnings<'src, T> {
617    /// Matched item. Typically either `MatchedItem<X>` or
618    /// `Option<MatchedItem<X>>`.
619    pub(crate) item: T,
620
621    /// Possible parse errors.
622    pub(crate) warnings: Vec<Warning<'src>>,
623}
624
625impl<T> MatchAndWarnings<'_, T> {
626    #[cfg(test)]
627    #[inline(always)]
628    #[track_caller]
629    #[allow(clippy::panic)] // since not actually in production code
630    pub(crate) fn unwrap_if_no_warnings(self) -> T {
631        if self.warnings.is_empty() {
632            self.item
633        } else {
634            panic!(
635                "expected self.warnings to be empty\n\nfound warnings = {warnings:#?}\n",
636                warnings = self.warnings
637            );
638        }
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    #![allow(clippy::unwrap_used)]
645
646    mod warning {
647        use crate::warnings::{Warning, WarningType};
648
649        #[test]
650        fn impl_clone() {
651            // Silly test to mark the #[derive(...)] line as covered.
652            let w1 = Warning {
653                source: crate::Span::new("abc"),
654                warning: WarningType::EmptyAttributeValue,
655                origin: None,
656            };
657
658            let w2 = w1.clone();
659            assert_eq!(w1, w2);
660        }
661    }
662
663    mod warning_type {
664        mod impl_debug {
665            use crate::warnings::WarningType;
666
667            #[test]
668            fn attribute_value_missing_terminating_quote() {
669                let warning = WarningType::AttributeValueMissingTerminatingQuote;
670                let debug_output = format!("{:?}", warning);
671                assert_eq!(
672                    debug_output,
673                    "WarningType::AttributeValueMissingTerminatingQuote"
674                );
675            }
676
677            #[test]
678            fn document_header_not_terminated() {
679                let warning = WarningType::DocumentHeaderNotTerminated;
680                let debug_output = format!("{:?}", warning);
681                assert_eq!(debug_output, "WarningType::DocumentHeaderNotTerminated");
682            }
683
684            #[test]
685            fn no_inline_doctype_candidate() {
686                let warning = WarningType::NoInlineDoctypeCandidate;
687                let debug_output = format!("{:?}", warning);
688                assert_eq!(debug_output, "WarningType::NoInlineDoctypeCandidate");
689            }
690
691            #[test]
692            fn empty_attribute_value() {
693                let warning = WarningType::EmptyAttributeValue;
694                let debug_output = format!("{:?}", warning);
695                assert_eq!(debug_output, "WarningType::EmptyAttributeValue");
696            }
697
698            #[test]
699            fn empty_shorthand_name() {
700                let warning = WarningType::EmptyShorthandName;
701                let debug_output = format!("{:?}", warning);
702                assert_eq!(debug_output, "WarningType::EmptyShorthandName");
703            }
704
705            #[test]
706            fn invalid_macro_name() {
707                let warning = WarningType::InvalidMacroName;
708                let debug_output = format!("{:?}", warning);
709                assert_eq!(debug_output, "WarningType::InvalidMacroName");
710            }
711
712            #[test]
713            fn media_macro_missing_target() {
714                let warning = WarningType::MediaMacroMissingTarget;
715                let debug_output = format!("{:?}", warning);
716                assert_eq!(debug_output, "WarningType::MediaMacroMissingTarget");
717            }
718
719            #[test]
720            fn macro_missing_attribute_list() {
721                let warning = WarningType::MacroMissingAttributeList;
722                let debug_output = format!("{:?}", warning);
723                assert_eq!(debug_output, "WarningType::MacroMissingAttributeList");
724            }
725
726            #[test]
727            fn macro_missing_separator() {
728                let warning = WarningType::MacroMissingSeparator;
729                let debug_output = format!("{:?}", warning);
730                assert_eq!(debug_output, "WarningType::MacroMissingSeparator");
731            }
732
733            #[test]
734            fn missing_comma_after_quoted_attribute_value() {
735                let warning = WarningType::MissingCommaAfterQuotedAttributeValue;
736                let debug_output = format!("{:?}", warning);
737                assert_eq!(
738                    debug_output,
739                    "WarningType::MissingCommaAfterQuotedAttributeValue"
740                );
741            }
742
743            #[test]
744            fn unterminated_delimited_block() {
745                let warning = WarningType::UnterminatedDelimitedBlock;
746                let debug_output = format!("{:?}", warning);
747                assert_eq!(debug_output, "WarningType::UnterminatedDelimitedBlock");
748            }
749
750            #[test]
751            fn missing_block_after_title_or_attribute_list() {
752                let warning = WarningType::MissingBlockAfterTitleOrAttributeList;
753                let debug_output = format!("{:?}", warning);
754                assert_eq!(
755                    debug_output,
756                    "WarningType::MissingBlockAfterTitleOrAttributeList"
757                );
758            }
759
760            #[test]
761            fn empty_block_anchor_name() {
762                let warning = WarningType::EmptyBlockAnchorName;
763                let debug_output = format!("{:?}", warning);
764                assert_eq!(debug_output, "WarningType::EmptyBlockAnchorName");
765            }
766
767            #[test]
768            fn invalid_block_anchor_name() {
769                let warning = WarningType::InvalidBlockAnchorName;
770                let debug_output = format!("{:?}", warning);
771                assert_eq!(debug_output, "WarningType::InvalidBlockAnchorName");
772            }
773
774            #[test]
775            fn attribute_value_is_locked_simple_string() {
776                let warning = WarningType::AttributeValueIsLocked("test-attribute".to_string());
777                let debug_output = format!("{:?}", warning);
778                assert_eq!(
779                    debug_output,
780                    "WarningType::AttributeValueIsLocked(\"test-attribute\")"
781                );
782            }
783
784            #[test]
785            fn attribute_value_is_locked_empty_string() {
786                let warning = WarningType::AttributeValueIsLocked("".to_string());
787                let debug_output = format!("{:?}", warning);
788                assert_eq!(debug_output, "WarningType::AttributeValueIsLocked(\"\")");
789            }
790
791            #[test]
792            fn attribute_value_is_locked_string_with_special_chars() {
793                let warning =
794                    WarningType::AttributeValueIsLocked("attr-with-special!@#$%^&*()".to_string());
795                let debug_output = format!("{:?}", warning);
796                assert_eq!(
797                    debug_output,
798                    "WarningType::AttributeValueIsLocked(\"attr-with-special!@#$%^&*()\")"
799                );
800            }
801
802            #[test]
803            fn attribute_value_is_locked_string_with_quotes() {
804                let warning = WarningType::AttributeValueIsLocked("attr\"with'quotes".to_string());
805                let debug_output = format!("{:?}", warning);
806                assert_eq!(
807                    debug_output,
808                    "WarningType::AttributeValueIsLocked(\"attr\\\"with'quotes\")"
809                );
810            }
811
812            #[test]
813            fn attribute_value_is_locked_string_with_newlines() {
814                let warning =
815                    WarningType::AttributeValueIsLocked("attr\nwith\nnewlines".to_string());
816                let debug_output = format!("{:?}", warning);
817                assert_eq!(
818                    debug_output,
819                    "WarningType::AttributeValueIsLocked(\"attr\\nwith\\nnewlines\")"
820                );
821            }
822
823            #[test]
824            fn duplicate_id() {
825                let warning = WarningType::DuplicateId("foo".to_owned());
826                let debug_output = format!("{:?}", warning);
827                assert_eq!(debug_output, "WarningType::DuplicateId(\"foo\")");
828            }
829
830            #[test]
831            fn level0_section_heading_not_supported() {
832                let warning = WarningType::Level0SectionHeadingNotSupported;
833                let debug_output = format!("{:?}", warning);
834                assert_eq!(
835                    debug_output,
836                    "WarningType::Level0SectionHeadingNotSupported"
837                );
838            }
839
840            #[test]
841            fn section_heading_level_skipped() {
842                let warning = WarningType::SectionHeadingLevelSkipped(2, 4);
843                let debug_output = format!("{:?}", warning);
844                assert_eq!(
845                    debug_output,
846                    "WarningType::SectionHeadingLevelSkipped(2, 4)"
847                );
848            }
849
850            #[test]
851            fn section_heading_level_exceeds_maximum() {
852                let warning = WarningType::SectionHeadingLevelExceedsMaximum(6);
853                let debug_output = format!("{:?}", warning);
854                assert_eq!(
855                    debug_output,
856                    "WarningType::SectionHeadingLevelExceedsMaximum(6)"
857                );
858            }
859
860            #[test]
861            fn section_heading_level_out_of_range() {
862                let warning = WarningType::SectionHeadingLevelOutOfRange(-3, 1);
863                let debug_output = format!("{:?}", warning);
864                assert_eq!(
865                    debug_output,
866                    "WarningType::SectionHeadingLevelOutOfRange(-3, 1)"
867                );
868            }
869
870            #[test]
871            fn leveloffset_excludes_all_heading_levels() {
872                let warning = WarningType::LeveloffsetExcludesAllHeadingLevels(2147483647);
873                let debug_output = format!("{:?}", warning);
874                assert_eq!(
875                    debug_output,
876                    "WarningType::LeveloffsetExcludesAllHeadingLevels(2147483647)"
877                );
878            }
879
880            #[test]
881            fn special_section_cannot_have_nested_sections() {
882                let warning =
883                    WarningType::SpecialSectionCannotHaveNestedSections("glossary".to_string());
884                let debug_output = format!("{:?}", warning);
885                assert_eq!(
886                    debug_output,
887                    "WarningType::SpecialSectionCannotHaveNestedSections(\"glossary\")"
888                );
889            }
890
891            #[test]
892            fn list_item_out_of_sequence() {
893                let warning = WarningType::ListItemOutOfSequence("y".to_string(), "z".to_string());
894                let debug_output = format!("{:?}", warning);
895                assert_eq!(
896                    debug_output,
897                    "WarningType::ListItemOutOfSequence(\"y\", \"z\")"
898                );
899            }
900
901            #[test]
902            fn no_callout_found() {
903                let warning = WarningType::NoCalloutFound(2);
904                let debug_output = format!("{:?}", warning);
905                assert_eq!(debug_output, "WarningType::NoCalloutFound(2)");
906            }
907
908            #[test]
909            fn callout_list_item_out_of_sequence() {
910                let warning = WarningType::CalloutListItemOutOfSequence(2, 3);
911                let debug_output = format!("{:?}", warning);
912                assert_eq!(
913                    debug_output,
914                    "WarningType::CalloutListItemOutOfSequence(2, 3)"
915                );
916            }
917
918            #[test]
919            fn table_cell_exceeds_column_count() {
920                let warning = WarningType::TableCellExceedsColumnCount;
921                let debug_output = format!("{:?}", warning);
922                assert_eq!(debug_output, "WarningType::TableCellExceedsColumnCount");
923            }
924
925            #[test]
926            fn table_csv_data_has_unclosed_quote() {
927                let warning = WarningType::TableCsvDataHasUnclosedQuote;
928                let debug_output = format!("{:?}", warning);
929                assert_eq!(debug_output, "WarningType::TableCsvDataHasUnclosedQuote");
930            }
931
932            #[test]
933            fn table_missing_leading_separator() {
934                let warning = WarningType::TableMissingLeadingSeparator;
935                let debug_output = format!("{:?}", warning);
936                assert_eq!(debug_output, "WarningType::TableMissingLeadingSeparator");
937            }
938
939            #[test]
940            fn table_incomplete_row_at_end_of_table() {
941                let warning = WarningType::TableIncompleteRowAtEndOfTable;
942                let debug_output = format!("{:?}", warning);
943                assert_eq!(debug_output, "WarningType::TableIncompleteRowAtEndOfTable");
944            }
945
946            #[test]
947            fn skipping_reference_to_missing_attribute() {
948                let warning = WarningType::SkippingReferenceToMissingAttribute("name".to_string());
949                let debug_output = format!("{:?}", warning);
950                assert_eq!(
951                    debug_output,
952                    "WarningType::SkippingReferenceToMissingAttribute(\"name\")"
953                );
954            }
955
956            #[test]
957            fn invalid_substitution_type_for_stem_macro() {
958                let warning = WarningType::InvalidSubstitutionTypeForStemMacro("bogus".to_string());
959                let debug_output = format!("{:?}", warning);
960                assert_eq!(
961                    debug_output,
962                    "WarningType::InvalidSubstitutionTypeForStemMacro(\"bogus\")"
963                );
964            }
965
966            #[test]
967            fn invalid_substitution_type_for_passthrough_macro() {
968                let warning =
969                    WarningType::InvalidSubstitutionTypeForPassthroughMacro("bogus".to_string());
970                let debug_output = format!("{:?}", warning);
971                assert_eq!(
972                    debug_output,
973                    "WarningType::InvalidSubstitutionTypeForPassthroughMacro(\"bogus\")"
974                );
975            }
976
977            #[test]
978            fn invalid_substitution_type_for_block() {
979                let warning = WarningType::InvalidSubstitutionTypeForBlock("bogus".to_string());
980                let debug_output = format!("{:?}", warning);
981                assert_eq!(
982                    debug_output,
983                    "WarningType::InvalidSubstitutionTypeForBlock(\"bogus\")"
984                );
985            }
986
987            #[test]
988            fn invalid_footnote_reference() {
989                let warning = WarningType::InvalidFootnoteReference("fn1".to_string());
990                let debug_output = format!("{:?}", warning);
991                assert_eq!(
992                    debug_output,
993                    "WarningType::InvalidFootnoteReference(\"fn1\")"
994                );
995            }
996
997            #[test]
998            fn deprecated_footnoteref_macro() {
999                let warning =
1000                    WarningType::DeprecatedFootnoterefMacro("footnoteref:[fn1]".to_string());
1001                let debug_output = format!("{:?}", warning);
1002                assert_eq!(
1003                    debug_output,
1004                    "WarningType::DeprecatedFootnoterefMacro(\"footnoteref:[fn1]\")"
1005                );
1006            }
1007
1008            #[test]
1009            fn include_file_not_found() {
1010                let warning = WarningType::IncludeFileNotFound("content.adoc".to_string());
1011                let debug_output = format!("{:?}", warning);
1012                assert_eq!(
1013                    debug_output,
1014                    "WarningType::IncludeFileNotFound(\"content.adoc\")"
1015                );
1016            }
1017
1018            #[test]
1019            fn include_file_not_readable() {
1020                let warning = WarningType::IncludeFileNotReadable("content.adoc".to_string());
1021                let debug_output = format!("{:?}", warning);
1022                assert_eq!(
1023                    debug_output,
1024                    "WarningType::IncludeFileNotReadable(\"content.adoc\")"
1025                );
1026            }
1027
1028            #[test]
1029            fn include_file_not_decodable() {
1030                let warning = WarningType::IncludeFileNotDecodable("content.adoc".to_string());
1031                let debug_output = format!("{:?}", warning);
1032                assert_eq!(
1033                    debug_output,
1034                    "WarningType::IncludeFileNotDecodable(\"content.adoc\")"
1035                );
1036            }
1037
1038            #[test]
1039            fn include_dropped_due_to_missing_attribute() {
1040                let warning = WarningType::IncludeDroppedDueToMissingAttribute(
1041                    "include::{foodir}/include-file.adoc[]".to_string(),
1042                );
1043
1044                let debug_output = format!("{:?}", warning);
1045
1046                assert_eq!(
1047                    debug_output,
1048                    "WarningType::IncludeDroppedDueToMissingAttribute(\"include::{foodir}/include-file.adoc[]\")"
1049                );
1050            }
1051
1052            #[test]
1053            fn max_include_depth_exceeded() {
1054                let warning = WarningType::MaxIncludeDepthExceeded(64);
1055                let debug_output = format!("{:?}", warning);
1056                assert_eq!(debug_output, "WarningType::MaxIncludeDepthExceeded(64)");
1057            }
1058
1059            #[test]
1060            fn max_block_nesting_exceeded() {
1061                let warning = WarningType::MaxBlockNestingExceeded(64);
1062                let debug_output = format!("{:?}", warning);
1063                assert_eq!(debug_output, "WarningType::MaxBlockNestingExceeded(64)");
1064            }
1065
1066            #[test]
1067            fn non_utf8_include_encoding() {
1068                let warning = WarningType::NonUtf8IncludeEncoding("iso-8859-1".to_string());
1069                let debug_output = format!("{:?}", warning);
1070                assert_eq!(
1071                    debug_output,
1072                    "WarningType::NonUtf8IncludeEncoding(\"iso-8859-1\")"
1073                );
1074            }
1075
1076            #[test]
1077            fn malformed_conditional_directive() {
1078                let warning = WarningType::MalformedConditionalDirective(
1079                    "missing target".to_string(),
1080                    "ifdef::[]".to_string(),
1081                );
1082                let debug_output = format!("{:?}", warning);
1083                assert_eq!(
1084                    debug_output,
1085                    "WarningType::MalformedConditionalDirective(\"missing target\", \"ifdef::[]\")"
1086                );
1087            }
1088
1089            #[test]
1090            fn unmatched_conditional_directive() {
1091                let warning =
1092                    WarningType::UnmatchedConditionalDirective("endif::on-quest[]".to_string());
1093                let debug_output = format!("{:?}", warning);
1094                assert_eq!(
1095                    debug_output,
1096                    "WarningType::UnmatchedConditionalDirective(\"endif::on-quest[]\")"
1097                );
1098            }
1099
1100            #[test]
1101            fn mismatched_conditional_directive() {
1102                let warning =
1103                    WarningType::MismatchedConditionalDirective("endif::on-journey[]".to_string());
1104                let debug_output = format!("{:?}", warning);
1105                assert_eq!(
1106                    debug_output,
1107                    "WarningType::MismatchedConditionalDirective(\"endif::on-journey[]\")"
1108                );
1109            }
1110
1111            #[test]
1112            fn unterminated_conditional_directive() {
1113                let warning =
1114                    WarningType::UnterminatedConditionalDirective("ifdef::on-quest[]".to_string());
1115                let debug_output = format!("{:?}", warning);
1116                assert_eq!(
1117                    debug_output,
1118                    "WarningType::UnterminatedConditionalDirective(\"ifdef::on-quest[]\")"
1119                );
1120            }
1121
1122            #[test]
1123            fn include_tag_not_found() {
1124                let warning = WarningType::IncludeTagNotFound("tag 'no-such-tag'".to_string());
1125                let debug_output = format!("{:?}", warning);
1126                assert_eq!(
1127                    debug_output,
1128                    "WarningType::IncludeTagNotFound(\"tag 'no-such-tag'\")"
1129                );
1130            }
1131
1132            #[test]
1133            fn include_tag_unclosed() {
1134                let warning = WarningType::IncludeTagUnclosed("'a'".to_string());
1135                let debug_output = format!("{:?}", warning);
1136                assert_eq!(debug_output, "WarningType::IncludeTagUnclosed(\"'a'\")");
1137            }
1138
1139            #[test]
1140            fn include_tag_mismatched_end() {
1141                let warning =
1142                    WarningType::IncludeTagMismatchedEnd("'b'".to_string(), "'a'".to_string());
1143                let debug_output = format!("{:?}", warning);
1144                assert_eq!(
1145                    debug_output,
1146                    "WarningType::IncludeTagMismatchedEnd(\"'b'\", \"'a'\")"
1147                );
1148            }
1149
1150            #[test]
1151            fn include_tag_unexpected_end() {
1152                let warning = WarningType::IncludeTagUnexpectedEnd("'a'".to_string());
1153                let debug_output = format!("{:?}", warning);
1154                assert_eq!(
1155                    debug_output,
1156                    "WarningType::IncludeTagUnexpectedEnd(\"'a'\")"
1157                );
1158            }
1159
1160            #[test]
1161            fn abstract_block_in_book_without_doctitle() {
1162                let warning = WarningType::AbstractBlockInBookWithoutDoctitle;
1163                let debug_output = format!("{:?}", warning);
1164                assert_eq!(
1165                    debug_output,
1166                    "WarningType::AbstractBlockInBookWithoutDoctitle"
1167                );
1168            }
1169
1170            #[test]
1171            fn possible_invalid_reference() {
1172                let warning = WarningType::PossibleInvalidReference("foobaz".to_string());
1173                let debug_output = format!("{:?}", warning);
1174                assert_eq!(
1175                    debug_output,
1176                    "WarningType::PossibleInvalidReference(\"foobaz\")"
1177                );
1178            }
1179
1180            #[test]
1181            fn unsafe_link_scheme_rejected() {
1182                let warning =
1183                    WarningType::UnsafeLinkSchemeRejected("javascript:alert(1)".to_string());
1184                let debug_output = format!("{:?}", warning);
1185                assert_eq!(
1186                    debug_output,
1187                    "WarningType::UnsafeLinkSchemeRejected(\"javascript:alert(1)\")"
1188                );
1189            }
1190        }
1191    }
1192
1193    mod match_and_warnings {
1194        use crate::warnings::{MatchAndWarnings, Warning, WarningType};
1195
1196        #[test]
1197        fn impl_clone() {
1198            // Silly test to mark the #[derive(...)] line as covered.
1199            let maw1 = MatchAndWarnings {
1200                item: "xyz",
1201                warnings: vec![Warning {
1202                    source: crate::Span::new("abc"),
1203                    warning: WarningType::EmptyAttributeValue,
1204                    origin: None,
1205                }],
1206            };
1207
1208            let maw2 = maw1.clone();
1209            assert_eq!(maw1, maw2);
1210        }
1211
1212        #[test]
1213        fn unwrap_if_no_warnings() {
1214            let maw = MatchAndWarnings {
1215                item: "xyz",
1216                warnings: vec![],
1217            };
1218
1219            let item = maw.unwrap_if_no_warnings();
1220            assert_eq!(item, "xyz");
1221        }
1222
1223        #[test]
1224        #[should_panic]
1225        fn unwrap_if_no_warnings_panic() {
1226            let maw = MatchAndWarnings {
1227                item: "xyz",
1228                warnings: vec![Warning {
1229                    source: crate::Span::new("abc"),
1230                    warning: WarningType::EmptyAttributeValue,
1231                    origin: None,
1232                }],
1233            };
1234
1235            let _ = maw.unwrap_if_no_warnings();
1236
1237            // There are warnings so this should panic.
1238        }
1239    }
1240}