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