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
+
+
+
+ Name |
+ Description |
+ Category |
+
+
+
+
+ traits |
+ static and dynamic dispatch |
+ generics |
+
+
+ enums |
+
+
+ doc comments |
+ reference documentation |
+
+
+ markdown comments |
+ attributes with custom syntax |
+
+
+
+```
+
+This will render the same way Markdown tables do, even though Markdown tables
+don't support `colspan`:
+
+
+
+
+ Name |
+ Description |
+ Category |
+
+
+
+
+ traits |
+ static and dynamic dispatch |
+ generics |
+
+
+ enums |
+
+
+ doc comments |
+ reference documentation |
+
+
+ markdown comments |
+ attributes 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
+