diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index a7c3c0bb60610..68028604fa463 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -5,6 +5,7 @@ #![feature(rustc_private)] #![feature(array_methods)] #![feature(assert_matches)] +#![feature(bool_to_option)] #![feature(box_patterns)] #![feature(control_flow_enum)] #![feature(box_syntax)] diff --git a/src/librustdoc/passes/html_tags.rs b/src/librustdoc/passes/html_tags.rs index f7a9a0899e390..9caadef3dec7c 100644 --- a/src/librustdoc/passes/html_tags.rs +++ b/src/librustdoc/passes/html_tags.rs @@ -38,7 +38,7 @@ fn drop_tag( tags: &mut Vec<(String, Range)>, tag_name: String, range: Range, - f: &impl Fn(&str, &Range), + f: &impl Fn(&str, &Range, bool), ) { let tag_name_low = tag_name.to_lowercase(); if let Some(pos) = tags.iter().rposition(|(t, _)| t.to_lowercase() == tag_name_low) { @@ -59,14 +59,42 @@ 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); + f(&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); + f(&format!("unopened HTML tag `{}`", tag_name), &range, false); + } +} + +fn extract_path_backwards(text: &str, end_pos: usize) -> Option { + use rustc_lexer::{is_id_continue, is_id_start}; + let mut current_pos = end_pos; + loop { + if current_pos >= 2 && text[..current_pos].ends_with("::") { + current_pos -= 2; + } + let new_pos = text[..current_pos] + .char_indices() + .rev() + .take_while(|(_, c)| is_id_start(*c) || is_id_continue(*c)) + .reduce(|_accum, item| item) + .and_then(|(new_pos, c)| is_id_start(c).then_some(new_pos)); + if let Some(new_pos) = new_pos { + if current_pos != new_pos { + current_pos = new_pos; + continue; + } + } + break; + } + if current_pos == end_pos { + return None; + } else { + return Some(current_pos); } } @@ -76,7 +104,7 @@ fn extract_html_tag( range: &Range, start_pos: usize, iter: &mut Peekable>, - f: &impl Fn(&str, &Range), + f: &impl Fn(&str, &Range, bool), ) { let mut tag_name = String::new(); let mut is_closing = false; @@ -140,7 +168,7 @@ fn extract_tags( text: &str, range: Range, is_in_comment: &mut Option>, - f: &impl Fn(&str, &Range), + f: &impl Fn(&str, &Range, bool), ) { let mut iter = text.char_indices().peekable(); @@ -178,14 +206,42 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> { }; let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); if !dox.is_empty() { - let report_diag = |msg: &str, range: &Range| { + let report_diag = |msg: &str, range: &Range, is_open_tag: bool| { let sp = match super::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, |lint| { - lint.build(msg).emit() + use rustc_lint_defs::Applicability; + let mut diag = lint.build(msg); + // 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. + if let Some(Some(generics_start)) = (is_open_tag + && dox[..range.end].ends_with(">")) + .then(|| extract_path_backwards(&dox, range.start)) + { + let generics_sp = match super::source_span_for_markdown_range( + tcx, + &dox, + &(generics_start..range.end), + &item.attrs, + ) { + Some(sp) => sp, + None => item.attr_span(tcx), + }; + // multipart form is chosen here because ``Vec`` would be confusing. + diag.multipart_suggestion( + "try marking as source code", + vec![ + (generics_sp.shrink_to_lo(), String::from("`")), + (generics_sp.shrink_to_hi(), String::from("`")), + ], + Applicability::MaybeIncorrect, + ); + } + diag.emit() }); }; @@ -210,11 +266,11 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> { let t = t.to_lowercase(); !ALLOWED_UNCLOSED.contains(&t.as_str()) }) { - report_diag(&format!("unclosed HTML tag `{}`", tag), range); + report_diag(&format!("unclosed HTML tag `{}`", tag), range, true); } if let Some(range) = is_in_comment { - report_diag("Unclosed HTML comment", &range); + report_diag("Unclosed HTML comment", &range, false); } } 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 new file mode 100644 index 0000000000000..744b3071f1b81 --- /dev/null +++ b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs @@ -0,0 +1,38 @@ +#![deny(rustdoc::invalid_html_tags)] + +/// This Vec<32> thing! +// Numbers aren't valid HTML tags, so no error. +pub struct ConstGeneric; + +/// This Vec thing! +// HTML tags cannot contain commas, so no error. +pub struct MultipleGenerics; + +/// This Vec thing! +//~^ERROR unclosed HTML tag `i32` +// HTML attributes shouldn't be treated as Rust syntax, so no suggestions. +pub struct TagWithAttributes; + +/// This Vec thing! +// There should be no error, and no suggestion, since the tags are balanced. +pub struct DoNotWarnOnMatchingTags; + +/// This Vec thing! +//~^ERROR unopened HTML tag `i32` +// This should produce an error, but no suggestion. +pub struct EndTagsAreNotValidRustSyntax; + +/// This 123 thing! +//~^ERROR unclosed HTML tag `i32` +// This should produce an error, but no suggestion. +pub struct NumbersAreNotPaths; + +/// This Vec: thing! +//~^ERROR unclosed HTML tag `i32` +// This should produce an error, but no suggestion. +pub struct InvalidTurbofish; + +/// This [link](https://rust-lang.org) thing! +//~^ERROR unclosed HTML tag `i32` +// This should produce an error, but no suggestion. +pub struct BareTurbofish; 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 new file mode 100644 index 0000000000000..832b8b2cac79a --- /dev/null +++ b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr @@ -0,0 +1,38 @@ +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics-no-suggestions.rs:11:13 + | +LL | /// This Vec thing! + | ^^^^ + | +note: the lint level is defined here + --> $DIR/html-as-generics-no-suggestions.rs:1:9 + | +LL | #![deny(rustdoc::invalid_html_tags)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unopened HTML tag `i32` + --> $DIR/html-as-generics-no-suggestions.rs:20:13 + | +LL | /// This Vec thing! + | ^^^^^^ + +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics-no-suggestions.rs:25:13 + | +LL | /// This 123 thing! + | ^^^^^ + +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics-no-suggestions.rs:30:14 + | +LL | /// This Vec: thing! + | ^^^^^ + +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics-no-suggestions.rs:35:39 + | +LL | /// This [link](https://rust-lang.org) thing! + | ^^^^^ + +error: aborting due to 5 previous errors + diff --git a/src/test/rustdoc-ui/suggestions/html-as-generics.fixed b/src/test/rustdoc-ui/suggestions/html-as-generics.fixed new file mode 100644 index 0000000000000..c0a0de24c5263 --- /dev/null +++ b/src/test/rustdoc-ui/suggestions/html-as-generics.fixed @@ -0,0 +1,32 @@ +// run-rustfix +#![deny(rustdoc::invalid_html_tags)] + +/// This `Vec` thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct Generic; + +/// This `vec::Vec` thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct GenericPath; + +/// This `i32` thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct PathsCanContainTrailingNumbers; + +/// This `Vec::` thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct Turbofish; + +/// This [link](https://rust-lang.org)`::` thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct BareTurbofish; + +/// This `Vec::` thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct Nested; diff --git a/src/test/rustdoc-ui/suggestions/html-as-generics.rs b/src/test/rustdoc-ui/suggestions/html-as-generics.rs new file mode 100644 index 0000000000000..0b6009b0e59c3 --- /dev/null +++ b/src/test/rustdoc-ui/suggestions/html-as-generics.rs @@ -0,0 +1,32 @@ +// run-rustfix +#![deny(rustdoc::invalid_html_tags)] + +/// This Vec thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct Generic; + +/// This vec::Vec thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct GenericPath; + +/// This i32 thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct PathsCanContainTrailingNumbers; + +/// This Vec:: thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct Turbofish; + +/// This [link](https://rust-lang.org):: thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct BareTurbofish; + +/// This Vec:: thing! +//~^ERROR unclosed HTML tag `i32` +//~|HELP try marking as source +pub struct Nested; diff --git a/src/test/rustdoc-ui/suggestions/html-as-generics.stderr b/src/test/rustdoc-ui/suggestions/html-as-generics.stderr new file mode 100644 index 0000000000000..df54b71264ebc --- /dev/null +++ b/src/test/rustdoc-ui/suggestions/html-as-generics.stderr @@ -0,0 +1,73 @@ +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics.rs:4:13 + | +LL | /// This Vec thing! + | ^^^^^ + | +note: the lint level is defined here + --> $DIR/html-as-generics.rs:2:9 + | +LL | #![deny(rustdoc::invalid_html_tags)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: try marking as source code + | +LL | /// This `Vec` thing! + | + + + +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics.rs:9:18 + | +LL | /// This vec::Vec thing! + | ^^^^^ + | +help: try marking as source code + | +LL | /// This `vec::Vec` thing! + | + + + +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics.rs:14:13 + | +LL | /// This i32 thing! + | ^^^^^ + | +help: try marking as source code + | +LL | /// This `i32` thing! + | + + + +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics.rs:19:15 + | +LL | /// This Vec:: thing! + | ^^^^^ + | +help: try marking as source code + | +LL | /// This `Vec::` thing! + | + + + +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics.rs:24:41 + | +LL | /// This [link](https://rust-lang.org):: thing! + | ^^^^^ + | +help: try marking as source code + | +LL | /// This [link](https://rust-lang.org)`::` thing! + | + + + +error: unclosed HTML tag `i32` + --> $DIR/html-as-generics.rs:29:21 + | +LL | /// This Vec:: thing! + | ^^^^^ + | +help: try marking as source code + | +LL | /// This `Vec::` thing! + | + + + +error: aborting due to 6 previous errors +