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