Skip to main content

asciidoc_parser/
warnings.rs

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