Skip to main content

asciidoc_parser/
warnings.rs

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