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