diff --git a/src/doc/rustdoc/src/how-to-write-documentation.md b/src/doc/rustdoc/src/how-to-write-documentation.md index 38fd1db5c21e8..f1eb54d687dab 100644 --- a/src/doc/rustdoc/src/how-to-write-documentation.md +++ b/src/doc/rustdoc/src/how-to-write-documentation.md @@ -254,6 +254,131 @@ characters: So, no need to manually enter those Unicode characters! +### Inline HTML + +As a standard Commonmark parser with no special restrictions, rustdoc allows +you to write HTML whenever the regular markup isn't sufficient, such as +advanced table layouts: + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionCategory
traitsstatic and dynamic dispatchgenerics
enums
doc commentsreference documentation
markdown commentsattributes with custom syntax
+``` + +This will render the same way Markdown tables do, even though Markdown tables +don't support `colspan`: + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionCategory
traitsstatic and dynamic dispatchgenerics
enums
doc commentsreference documentation
markdown commentsattributes with custom syntax
+ +HTML is not sanitized when included in the documentation, though +improperly-nested tags will produce a build-time warning. + +```text +warning: unclosed HTML tag `h2` + --> $DIR/invalid-html-tags.rs:19:7 + | +LL | ///

+ | ^^^^ + +warning: unclosed quoted HTML attribute on tag `p` + --> $DIR/invalid-html-self-closing-tag.rs:19:14 + | +LL | ///

+ | -^^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +warning: unprefixed HTML `id` attribute + --> $DIR/unprefixed-html-id.rs:4:24 + | +LL | /// Test with

+ | -^^^ + | | + | help: add prefix: `unprefixed_html_id_` + | + +warning: 2 warnings emitted +``` + +We recommend the following additional restrictions: + + * Start all doc comments with a one-sentence summary that doesn't use + inline HTML. This summary will be used in contexts where arbitrary HTML + cannot, such as tooltips. + * Though JavaScript is allowed, many viewers won't run it. Ensure your docs + are readable without JavaScript. + * Do not embed CSS or JavaScript in doc comments to customize rustdoc's + UI. If you want to publish documentation with a customized UI, invoke + rustdoc with the `--html-in-header` [command-line parameter] to generate it + with your custom stylesheet or script, then publish the result as + pre-built HTML. + [`backtrace`]: https://docs.rs/backtrace/0.3.50/backtrace/ [commonmark markdown specification]: https://commonmark.org/ [commonmark quick reference]: https://commonmark.org/help/ @@ -268,3 +393,4 @@ So, no need to manually enter those Unicode characters! [strikethrough]: https://github.github.com/gfm/#strikethrough-extension- [tables]: https://github.github.com/gfm/#tables-extension- [task list extension]: https://github.github.com/gfm/#task-list-items-extension- +[command-line parameter]: command-line-arguments.md#--html-in-header-include-more-html-in diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs index 3aad97bc296fb..73d870c9f8709 100644 --- a/src/librustdoc/lint.rs +++ b/src/librustdoc/lint.rs @@ -152,6 +152,26 @@ declare_rustdoc_lint! { "detects invalid HTML tags in doc comments" } +declare_rustdoc_lint! { + /// The `unprefixed_html_id` lint detects potential HTML ID conflicts. This is a + /// `rustdoc` only lint, see the documentation in the [rustdoc book]. + /// + /// [rustdoc book]: ../../../rustdoc/lints.html#unprefixed_html_ids + UNPREFIXED_HTML_ID, + Warn, + "detects HTML id attributes that do not start with the crate name" +} + +declare_rustdoc_lint! { + /// The `unprefixed_html_class` lint detects potential HTML class conflicts. This is a + /// `rustdoc` only lint, see the documentation in the [rustdoc book]. + /// + /// [rustdoc book]: ../../../rustdoc/lints.html#unprefixed_html_ids + UNPREFIXED_HTML_CLASS, + Warn, + "detects HTML class attributes that do not start with the crate name" +} + declare_rustdoc_lint! { /// The `bare_urls` lint detects when a URL is not a hyperlink. /// This is a `rustdoc` only lint, see the documentation in the [rustdoc book]. @@ -185,6 +205,8 @@ pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { INVALID_HTML_TAGS, BARE_URLS, MISSING_CRATE_LEVEL_DOCS, + UNPREFIXED_HTML_ID, + UNPREFIXED_HTML_CLASS, ] }); diff --git a/src/librustdoc/passes/lint/html_tags.rs b/src/librustdoc/passes/lint/html_tags.rs index 070c0aab5868b..f2575d4fb39db 100644 --- a/src/librustdoc/passes/lint/html_tags.rs +++ b/src/librustdoc/passes/lint/html_tags.rs @@ -4,6 +4,7 @@ use crate::core::DocContext; use crate::html::markdown::main_body_opts; use crate::passes::source_span_for_markdown_range; +use itertools::Itertools; use pulldown_cmark::{BrokenLink, Event, LinkType, Parser, Tag}; use std::iter::Peekable; @@ -17,92 +18,6 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) { else { return }; let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); if !dox.is_empty() { - let report_diag = |msg: &str, range: &Range, is_open_tag: bool| { - let sp = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) { - Some(sp) => sp, - None => item.attr_span(tcx), - }; - tcx.struct_span_lint_hir(crate::lint::INVALID_HTML_TAGS, hir_id, sp, msg, |lint| { - use rustc_lint_defs::Applicability; - // If a tag looks like ``, it might actually be a generic. - // We don't try to detect stuff `` because that's not valid HTML, - // and we don't try to detect stuff `` because that's not valid Rust. - let mut generics_end = range.end; - if let Some(Some(mut generics_start)) = (is_open_tag - && dox[..generics_end].ends_with('>')) - .then(|| extract_path_backwards(&dox, range.start)) - { - while generics_start != 0 - && generics_end < dox.len() - && dox.as_bytes()[generics_start - 1] == b'<' - && dox.as_bytes()[generics_end] == b'>' - { - generics_end += 1; - generics_start -= 1; - if let Some(new_start) = extract_path_backwards(&dox, generics_start) { - generics_start = new_start; - } - if let Some(new_end) = extract_path_forward(&dox, generics_end) { - generics_end = new_end; - } - } - if let Some(new_end) = extract_path_forward(&dox, generics_end) { - generics_end = new_end; - } - let generics_sp = match source_span_for_markdown_range( - tcx, - &dox, - &(generics_start..generics_end), - &item.attrs, - ) { - Some(sp) => sp, - None => item.attr_span(tcx), - }; - // Sometimes, we only extract part of a path. For example, consider this: - // - // <[u32] as IntoIter>::Item - // ^^^^^ unclosed HTML tag `u32` - // - // We don't have any code for parsing fully-qualified trait paths. - // In theory, we could add it, but doing it correctly would require - // parsing the entire path grammar, which is problematic because of - // overlap between the path grammar and Markdown. - // - // The example above shows that ambiguity. Is `[u32]` intended to be an - // intra-doc link to the u32 primitive, or is it intended to be a slice? - // - // If the below conditional were removed, we would suggest this, which is - // not what the user probably wants. - // - // <[u32] as `IntoIter`>::Item - // - // We know that the user actually wants to wrap the whole thing in a code - // block, but the only reason we know that is because `u32` does not, in - // fact, implement IntoIter. If the example looks like this: - // - // <[Vec] as IntoIter::Item - // - // The ideal fix would be significantly different. - if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<') - || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>') - { - return lint; - } - // multipart form is chosen here because ``Vec`` would be confusing. - lint.multipart_suggestion( - "try marking as source code", - vec![ - (generics_sp.shrink_to_lo(), String::from("`")), - (generics_sp.shrink_to_hi(), String::from("`")), - ], - Applicability::MaybeIncorrect, - ); - } - - lint - }); - }; - let mut tags = Vec::new(); let mut is_in_comment = None; let mut in_code_block = false; @@ -132,6 +47,8 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) { let p = Parser::new_with_broken_link_callback(&dox, main_body_opts(), Some(&mut replacer)) .into_offset_iter(); + let report_diag = ReportDiag { cx, dox: dox.clone(), hir_id, item }; + for (event, range) in p { match event { Event::Start(Tag::CodeBlock(_)) => in_code_block = true, @@ -147,11 +64,11 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) { let t = t.to_lowercase(); !ALLOWED_UNCLOSED.contains(&t.as_str()) }) { - report_diag(&format!("unclosed HTML tag `{}`", tag), range, true); + report_diag.invalid_html_tags(&format!("unclosed HTML tag `{}`", tag), range, true); } if let Some(range) = is_in_comment { - report_diag("Unclosed HTML comment", &range, false); + report_diag.invalid_html_tags("Unclosed HTML comment", &range, false); } } } @@ -165,7 +82,7 @@ fn drop_tag( tags: &mut Vec<(String, Range)>, tag_name: String, range: Range, - f: &impl Fn(&str, &Range, bool), + report_diag: &ReportDiag<'_, '_>, ) { let tag_name_low = tag_name.to_lowercase(); if let Some(pos) = tags.iter().rposition(|(t, _)| t.to_lowercase() == tag_name_low) { @@ -186,14 +103,18 @@ fn drop_tag( // `tags` is used as a queue, meaning that everything after `pos` is included inside it. // So `

` will look like `["h2", "h3"]`. So when closing `h2`, we will still // have `h3`, meaning the tag wasn't closed as it should have. - f(&format!("unclosed HTML tag `{}`", last_tag_name), &last_tag_span, true); + report_diag.invalid_html_tags( + &format!("unclosed HTML tag `{}`", last_tag_name), + &last_tag_span, + true, + ); } // Remove the `tag_name` that was originally closed tags.pop(); } else { // It can happen for example in this case: `

` (the `h2` tag isn't required // but it helps for the visualization). - f(&format!("unopened HTML tag `{}`", tag_name), &range, false); + report_diag.invalid_html_tags(&format!("unopened HTML tag `{}`", tag_name), &range, false); } } @@ -257,13 +178,59 @@ fn is_valid_for_html_tag_name(c: char, is_empty: bool) -> bool { c.is_ascii_alphabetic() || !is_empty && (c == '-' || c.is_ascii_digit()) } +/// These class names are provided by rustdoc to doc authors as a stable API. +/// +/// Structure: `[(class, [depends_on_other_class])]` +const STABLE_CLASSES: &[(&str, &[&str])] = + &[("stab", &[]), ("deprecated", &["stab"]), ("portability", &["stab"])]; + +fn check_html_attr(name: &str, value: &str, range: Range, report_diag: &ReportDiag<'_, '_>) { + match name { + "class" => { + let prefix = format!("{}_", report_diag.crate_name()); + let has_prefix = |name: &str| name.starts_with(&prefix); + let is_stable_class = |name: &str| { + STABLE_CLASSES.iter().any(|&(class, depends_on_other_classes)| { + class == name + && depends_on_other_classes.iter().all(|&class| { + value.split_ascii_whitespace().any(|class2| class2 == class) + }) + }) + }; + // Can't use `split_ascii_whitespace()`, because I need byte offsets to report suggestions. + let mut start = None; + for (i, c) in value.char_indices() { + if c.is_ascii_whitespace() { + if let Some(start) = start && let class_name = &value[start..i] && !has_prefix(class_name) && !is_stable_class(class_name) { + let range = (start + range.start)..(i + range.start); + report_diag.unprefixed_html_class(&range); + } + start = None; + } else if start.is_none() { + start = Some(i); + } + } + if let Some(start) = start && let class_name = &value[start..] && !has_prefix(class_name) && !is_stable_class(class_name) { + let range = (start + range.start)..range.end; + report_diag.unprefixed_html_class(&range); + } + } + "id" => { + if !value.starts_with(&format!("{}_", report_diag.crate_name())) { + report_diag.unprefixed_html_id(&range); + } + } + _ => {} + } +} + fn extract_html_tag( tags: &mut Vec<(String, Range)>, text: &str, range: &Range, start_pos: usize, iter: &mut Peekable>, - f: &impl Fn(&str, &Range, bool), + report_diag: &ReportDiag<'_, '_>, ) { let mut tag_name = String::new(); let mut is_closing = false; @@ -293,13 +260,13 @@ fn extract_html_tag( if is_closing { // In case we have "" or even "". if c != '>' { - if !c.is_whitespace() { + if !c.is_ascii_whitespace() { // It seems like it's not a valid HTML tag. break; } let mut found = false; for (new_pos, c) in text[pos..].char_indices() { - if !c.is_whitespace() { + if !c.is_ascii_whitespace() { if c == '>' { r.end = range.start + new_pos + 1; found = true; @@ -311,61 +278,119 @@ fn extract_html_tag( break; } } - drop_tag(tags, tag_name, r, f); + drop_tag(tags, tag_name, r, report_diag); } else { - let mut is_self_closing = false; - let mut quote_pos = None; - if c != '>' { - let mut quote = None; - let mut after_eq = false; - for (i, c) in text[pos..].char_indices() { - if !c.is_whitespace() { - if let Some(q) = quote { - if c == q { - quote = None; - quote_pos = None; - after_eq = false; - } - } else if c == '>' { + #[derive(Clone, Copy, Eq, PartialEq)] + enum HtmlAttrParseState { + Start { start_pos: usize }, + PotentiallySelfClosing, + AfterEq { start_pos: usize, eq_pos: usize }, + Quoted { start_pos: usize, eq_pos: usize, quote_pos: usize, quote: char }, + Unquoted { start_pos: usize, eq_pos: usize }, + } + let mut state = HtmlAttrParseState::Start { start_pos: pos }; + for (i, c) in text[pos..].char_indices() { + let cur_pos = pos + i; + match state { + HtmlAttrParseState::Start { start_pos } => { + if c.is_ascii_whitespace() { + state = HtmlAttrParseState::Start { start_pos: cur_pos + 1 }; + } else if c == '/' { + state = HtmlAttrParseState::PotentiallySelfClosing; + } else if c == '=' { + state = + HtmlAttrParseState::AfterEq { start_pos, eq_pos: cur_pos } + } + } + HtmlAttrParseState::PotentiallySelfClosing => { + if c == '>' { break; - } else if c == '/' && !after_eq { - is_self_closing = true; + } else if !c.is_ascii_whitespace() { + state = HtmlAttrParseState::Start { start_pos: cur_pos }; + } + } + HtmlAttrParseState::AfterEq { start_pos, eq_pos } => { + if c == '>' { + break; + } else if c == '"' || c == '\'' { + state = HtmlAttrParseState::Quoted { + start_pos, + eq_pos, + quote_pos: cur_pos, + quote: c, + }; + } else if c.is_ascii_whitespace() { + check_html_attr( + &text[start_pos..eq_pos], + "", + (r.start + eq_pos)..(r.start + eq_pos), + report_diag, + ); + state = HtmlAttrParseState::Start { start_pos: cur_pos + 1 }; } else { - if is_self_closing { - is_self_closing = false; - } - if (c == '"' || c == '\'') && after_eq { - quote = Some(c); - quote_pos = Some(pos + i); - } else if c == '=' { - after_eq = true; - } + state = HtmlAttrParseState::Unquoted { start_pos, eq_pos } + } + } + HtmlAttrParseState::Quoted { start_pos, eq_pos, quote_pos, quote } => { + if c == quote { + check_html_attr( + &text[start_pos..eq_pos], + &text[(quote_pos + 1)..cur_pos], + (quote_pos + 1 + r.start)..(cur_pos + r.start), + report_diag, + ); + state = HtmlAttrParseState::Start { start_pos: cur_pos + 1 }; + } + } + HtmlAttrParseState::Unquoted { start_pos, eq_pos } => { + if c == '>' { + check_html_attr( + &text[start_pos..eq_pos], + &text[(eq_pos + 1)..cur_pos], + (eq_pos + 1 + r.start)..(cur_pos + r.start), + report_diag, + ); + break; + } else if c.is_ascii_whitespace() { + check_html_attr( + &text[start_pos..eq_pos], + &text[(eq_pos + 1)..cur_pos], + (eq_pos + 1 + r.start)..(cur_pos + r.start), + report_diag, + ); + state = HtmlAttrParseState::Start { start_pos: cur_pos + 1 }; } - } else if quote.is_none() { - after_eq = false; } } } - if let Some(quote_pos) = quote_pos { - let qr = Range { start: quote_pos, end: quote_pos }; - f( - &format!("unclosed quoted HTML attribute on tag `{}`", tag_name), - &qr, - false, - ); - } - if is_self_closing { - // https://html.spec.whatwg.org/#parse-error-non-void-html-element-start-tag-with-trailing-solidus - let valid = ALLOWED_UNCLOSED.contains(&&tag_name[..]) - || tags.iter().take(pos + 1).any(|(at, _)| { - let at = at.to_lowercase(); - at == "svg" || at == "math" - }); - if !valid { - f(&format!("invalid self-closing HTML tag `{}`", tag_name), &r, false); + match state { + HtmlAttrParseState::PotentiallySelfClosing => { + // https://html.spec.whatwg.org/#parse-error-non-void-html-element-start-tag-with-trailing-solidus + let valid = ALLOWED_UNCLOSED.contains(&&tag_name[..]) + || tags.iter().take(pos + 1).any(|(at, _)| { + let at = at.to_lowercase(); + at == "svg" || at == "math" + }); + if !valid { + report_diag.invalid_html_tags( + &format!("invalid self-closing HTML tag `{}`", tag_name), + &r, + false, + ); + } + } + HtmlAttrParseState::Quoted { quote_pos, .. } => { + let qr = Range { start: quote_pos, end: quote_pos }; + report_diag.invalid_html_tags( + &format!("unclosed quoted HTML attribute on tag `{}`", tag_name), + &qr, + false, + ); + tags.push((tag_name, r)); + } + _ => { + tags.push((tag_name, r)); } - } else { - tags.push((tag_name, r)); } } } @@ -380,7 +405,7 @@ fn extract_tags( text: &str, range: Range, is_in_comment: &mut Option>, - f: &impl Fn(&str, &Range, bool), + report_diag: &ReportDiag<'_, '_>, ) { let mut iter = text.char_indices().peekable(); @@ -400,8 +425,162 @@ fn extract_tags( end: range.start + start_pos + 3, }); } else { - extract_html_tag(tags, text, &range, start_pos, &mut iter, f); + extract_html_tag(tags, text, &range, start_pos, &mut iter, report_diag); } } } } + +struct ReportDiag<'cx, 'item> { + cx: &'item DocContext<'cx>, + dox: String, + hir_id: rustc_hir::HirId, + item: &'item Item, +} + +impl<'cx, 'item> ReportDiag<'cx, 'item> { + fn invalid_html_tags(&self, msg: &str, range: &Range, is_open_tag: bool) { + let ReportDiag { cx, ref dox, hir_id, item } = *self; + let tcx = cx.tcx; + let sp = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) { + Some(sp) => sp, + None => item.attr_span(tcx), + }; + tcx.struct_span_lint_hir(crate::lint::INVALID_HTML_TAGS, hir_id, sp, msg, |lint| { + use rustc_lint_defs::Applicability; + // If a tag looks like ``, it might actually be a generic. + // We don't try to detect stuff `` because that's not valid HTML, + // and we don't try to detect stuff `` because that's not valid Rust. + let mut generics_end = range.end; + if let Some(Some(mut generics_start)) = (is_open_tag + && dox[..generics_end].ends_with('>')) + .then(|| extract_path_backwards(&dox, range.start)) + { + while generics_start != 0 + && generics_end < dox.len() + && dox.as_bytes()[generics_start - 1] == b'<' + && dox.as_bytes()[generics_end] == b'>' + { + generics_end += 1; + generics_start -= 1; + if let Some(new_start) = extract_path_backwards(&dox, generics_start) { + generics_start = new_start; + } + if let Some(new_end) = extract_path_forward(&dox, generics_end) { + generics_end = new_end; + } + } + if let Some(new_end) = extract_path_forward(&dox, generics_end) { + generics_end = new_end; + } + let generics_sp = match source_span_for_markdown_range( + tcx, + &dox, + &(generics_start..generics_end), + &item.attrs, + ) { + Some(sp) => sp, + None => item.attr_span(tcx), + }; + // Sometimes, we only extract part of a path. For example, consider this: + // + // <[u32] as IntoIter>::Item + // ^^^^^ unclosed HTML tag `u32` + // + // We don't have any code for parsing fully-qualified trait paths. + // In theory, we could add it, but doing it correctly would require + // parsing the entire path grammar, which is problematic because of + // overlap between the path grammar and Markdown. + // + // The example above shows that ambiguity. Is `[u32]` intended to be an + // intra-doc link to the u32 primitive, or is it intended to be a slice? + // + // If the below conditional were removed, we would suggest this, which is + // not what the user probably wants. + // + // <[u32] as `IntoIter`>::Item + // + // We know that the user actually wants to wrap the whole thing in a code + // block, but the only reason we know that is because `u32` does not, in + // fact, implement IntoIter. If the example looks like this: + // + // <[Vec] as IntoIter::Item + // + // The ideal fix would be significantly different. + if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<') + || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>') + { + return lint; + } + // multipart form is chosen here because ``Vec`` would be confusing. + lint.multipart_suggestion( + "try marking as source code", + vec![ + (generics_sp.shrink_to_lo(), String::from("`")), + (generics_sp.shrink_to_hi(), String::from("`")), + ], + Applicability::MaybeIncorrect, + ); + } + + lint + }); + } + fn crate_name(&self) -> rustc_span::Symbol { + self.cx.tcx.crate_name(self.item.item_id.krate()) + } + fn unprefixed_html_class(&self, range: &Range) { + let ReportDiag { cx, ref dox, hir_id, item } = *self; + let tcx = cx.tcx; + let span = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) { + Some(span) => span, + None => item.attr_span(tcx), + }; + let msg = "unprefixed HTML `class` attribute"; + tcx.struct_span_lint_hir(crate::lint::UNPREFIXED_HTML_CLASS, hir_id, span, msg, |lint| { + use rustc_lint_defs::Applicability; + lint.multipart_suggestion( + "add prefix", + vec![(span.shrink_to_lo(), format!("{}_", self.crate_name()))], + Applicability::MaybeIncorrect, + ); + lint.help(format!( + "classes should start with `{{cratename}}_`, or be: {}", + STABLE_CLASSES + .iter() + .enumerate() + .map(|(i, &(class, depends_on_other_classes))| { + let mut result = + if i == STABLE_CLASSES.len() - 1 { "or `" } else { "`" }.to_string(); + for other in depends_on_other_classes { + result.push_str(other); + result.push_str(" "); + } + result.push_str(class); + result.push_str("`"); + result + }) + .join(", ") + )); + lint + }); + } + fn unprefixed_html_id(&self, range: &Range) { + let ReportDiag { cx, ref dox, hir_id, item } = *self; + let tcx = cx.tcx; + let span = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) { + Some(span) => span, + None => item.attr_span(tcx), + }; + let msg = "unprefixed HTML `id` attribute"; + tcx.struct_span_lint_hir(crate::lint::UNPREFIXED_HTML_ID, hir_id, span, msg, |lint| { + use rustc_lint_defs::Applicability; + lint.multipart_suggestion( + "add prefix", + vec![(span.shrink_to_lo(), format!("{}_", self.crate_name()))], + Applicability::MachineApplicable, + ); + lint + }); + } +} diff --git a/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs index 476e3b2d43e4a..180a544d94949 100644 --- a/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs +++ b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs @@ -50,7 +50,7 @@ pub struct FullyQualifiedPathsDoNotCount3; // out if this is valid would require parsing the entire path grammar. pub struct FullyQualifiedPathsDoNotCount4; -/// This Vec thing! +/// This Vec thing! //~^ERROR unclosed HTML tag `i32` // HTML attributes shouldn't be treated as Rust syntax, so no suggestions. pub struct TagWithAttributes; diff --git a/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr index 3856a251321b2..81bc0aac02ca5 100644 --- a/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr +++ b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr @@ -37,7 +37,7 @@ LL | /// This Vec as IntoIter> thing! error: unclosed HTML tag `i32` --> $DIR/html-as-generics-no-suggestions.rs:53:13 | -LL | /// This Vec thing! +LL | /// This Vec thing! | ^^^^ error: unopened HTML tag `i32` diff --git a/src/test/rustdoc-ui/unprefixed-html-class.fixed b/src/test/rustdoc-ui/unprefixed-html-class.fixed new file mode 100644 index 0000000000000..a54f5cf41e496 --- /dev/null +++ b/src/test/rustdoc-ui/unprefixed-html-class.fixed @@ -0,0 +1,56 @@ +// run-rustfix +#![deny(rustdoc::unprefixed_html_class)] + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct BasicBad; + +/// Test with
+pub struct BasicGood; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct MixedBadAndGood1; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct MixedBadAndGood2; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +//~^^ ERROR unprefixed HTML `class` attribute +pub struct DoubleBad; + +// `stab` is currently the only class name that's allowed unprefixed. + +/// Test with
+/// Test with
+/// Test with
+/// Test with
+/// Test with
+/// Test with
+/// Test with
+pub struct Stab; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct StandaloneDeprecatedIsBad; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct StandalonePortabilityIsBad; + +/// Test unquoted:
+pub struct UnquotedStab; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `class` attribute +pub struct UnquotedBad; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `class` attribute +pub struct UnquotedBadMixed1; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `class` attribute +pub struct UnquotedBadMixed2; diff --git a/src/test/rustdoc-ui/unprefixed-html-class.rs b/src/test/rustdoc-ui/unprefixed-html-class.rs new file mode 100644 index 0000000000000..c2b56d1b9dc47 --- /dev/null +++ b/src/test/rustdoc-ui/unprefixed-html-class.rs @@ -0,0 +1,56 @@ +// run-rustfix +#![deny(rustdoc::unprefixed_html_class)] + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct BasicBad; + +/// Test with
+pub struct BasicGood; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct MixedBadAndGood1; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct MixedBadAndGood2; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +//~^^ ERROR unprefixed HTML `class` attribute +pub struct DoubleBad; + +// `stab` is currently the only class name that's allowed unprefixed. + +/// Test with
+/// Test with
+/// Test with
+/// Test with
+/// Test with
+/// Test with
+/// Test with
+pub struct Stab; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct StandaloneDeprecatedIsBad; + +/// Test with
+//~^ ERROR unprefixed HTML `class` attribute +pub struct StandalonePortabilityIsBad; + +/// Test unquoted:
+pub struct UnquotedStab; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `class` attribute +pub struct UnquotedBad; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `class` attribute +pub struct UnquotedBadMixed1; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `class` attribute +pub struct UnquotedBadMixed2; diff --git a/src/test/rustdoc-ui/unprefixed-html-class.stderr b/src/test/rustdoc-ui/unprefixed-html-class.stderr new file mode 100644 index 0000000000000..f9f0c91f7c3b6 --- /dev/null +++ b/src/test/rustdoc-ui/unprefixed-html-class.stderr @@ -0,0 +1,107 @@ +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:4:27 + | +LL | /// Test with
+ | -^^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` +note: the lint level is defined here + --> $DIR/unprefixed-html-class.rs:2:9 + | +LL | #![deny(rustdoc::unprefixed_html_class)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:11:54 + | +LL | /// Test with
+ | -^^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:15:27 + | +LL | /// Test with
+ | -^^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:19:27 + | +LL | /// Test with
+ | -^^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:19:32 + | +LL | /// Test with
+ | -^^^^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:35:27 + | +LL | /// Test with
+ | -^^^^^^^^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:39:27 + | +LL | /// Test with
+ | -^^^^^^^^^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:46:31 + | +LL | /// Test unquoted:
+ | -^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:50:31 + | +LL | /// Test unquoted:
+ | -^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: unprefixed HTML `class` attribute + --> $DIR/unprefixed-html-class.rs:54:40 + | +LL | /// Test unquoted:
+ | -^^ + | | + | help: add prefix: `unprefixed_html_class_` + | + = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability` + +error: aborting due to 10 previous errors + diff --git a/src/test/rustdoc-ui/unprefixed-html-id.fixed b/src/test/rustdoc-ui/unprefixed-html-id.fixed new file mode 100644 index 0000000000000..bf3f3ef1a56f0 --- /dev/null +++ b/src/test/rustdoc-ui/unprefixed-html-id.fixed @@ -0,0 +1,27 @@ +// run-rustfix +#![deny(rustdoc::unprefixed_html_id)] + +/// Test with
+//~^ ERROR unprefixed HTML `id` attribute +pub struct BasicBad; + +/// Test with
+pub struct BasicGood; + +// `stab` is not allowed as a special ID. + +/// Test with
+//~^ ERROR unprefixed HTML `id` attribute +pub struct StabIsNotAnId; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `id` attribute +pub struct UnquotedStab; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `id` attribute +pub struct UnquotedBadMixed1; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `id` attribute +pub struct UnquotedBadMixed2; diff --git a/src/test/rustdoc-ui/unprefixed-html-id.rs b/src/test/rustdoc-ui/unprefixed-html-id.rs new file mode 100644 index 0000000000000..71d4cba7d2b73 --- /dev/null +++ b/src/test/rustdoc-ui/unprefixed-html-id.rs @@ -0,0 +1,27 @@ +// run-rustfix +#![deny(rustdoc::unprefixed_html_id)] + +/// Test with
+//~^ ERROR unprefixed HTML `id` attribute +pub struct BasicBad; + +/// Test with
+pub struct BasicGood; + +// `stab` is not allowed as a special ID. + +/// Test with
+//~^ ERROR unprefixed HTML `id` attribute +pub struct StabIsNotAnId; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `id` attribute +pub struct UnquotedStab; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `id` attribute +pub struct UnquotedBadMixed1; + +/// Test unquoted:
+//~^ ERROR unprefixed HTML `id` attribute +pub struct UnquotedBadMixed2; diff --git a/src/test/rustdoc-ui/unprefixed-html-id.stderr b/src/test/rustdoc-ui/unprefixed-html-id.stderr new file mode 100644 index 0000000000000..0c27a3bd4b7d2 --- /dev/null +++ b/src/test/rustdoc-ui/unprefixed-html-id.stderr @@ -0,0 +1,48 @@ +error: unprefixed HTML `id` attribute + --> $DIR/unprefixed-html-id.rs:4:24 + | +LL | /// Test with
+ | -^^^ + | | + | help: add prefix: `unprefixed_html_id_` + | +note: the lint level is defined here + --> $DIR/unprefixed-html-id.rs:2:9 + | +LL | #![deny(rustdoc::unprefixed_html_id)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unprefixed HTML `id` attribute + --> $DIR/unprefixed-html-id.rs:13:24 + | +LL | /// Test with
+ | -^^^ + | | + | help: add prefix: `unprefixed_html_id_` + +error: unprefixed HTML `id` attribute + --> $DIR/unprefixed-html-id.rs:17:28 + | +LL | /// Test unquoted:
+ | -^^^ + | | + | help: add prefix: `unprefixed_html_id_` + +error: unprefixed HTML `id` attribute + --> $DIR/unprefixed-html-id.rs:21:28 + | +LL | /// Test unquoted:
+ | -^^ + | | + | help: add prefix: `unprefixed_html_id_` + +error: unprefixed HTML `id` attribute + --> $DIR/unprefixed-html-id.rs:25:37 + | +LL | /// Test unquoted:
+ | -^^ + | | + | help: add prefix: `unprefixed_html_id_` + +error: aborting due to 5 previous errors +