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