From 5515fc88dc45c274f0574d381a17d4f72dfd5047 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 25 Apr 2023 15:04:22 +0200 Subject: [PATCH 01/13] Implement custom classes for rustdoc code blocks with `custom_code_classes_in_docs` feature --- compiler/rustc_feature/src/active.rs | 2 + compiler/rustc_span/src/symbol.rs | 1 + src/librustdoc/html/highlight.rs | 25 +- src/librustdoc/html/markdown.rs | 236 ++++++++++++++---- src/librustdoc/html/markdown/tests.rs | 24 ++ .../passes/check_custom_code_classes.rs | 77 ++++++ src/librustdoc/passes/mod.rs | 5 + 7 files changed, 321 insertions(+), 49 deletions(-) create mode 100644 src/librustdoc/passes/check_custom_code_classes.rs diff --git a/compiler/rustc_feature/src/active.rs b/compiler/rustc_feature/src/active.rs index fcb112eadfedb..6c338be99b61e 100644 --- a/compiler/rustc_feature/src/active.rs +++ b/compiler/rustc_feature/src/active.rs @@ -401,6 +401,8 @@ declare_features! ( /// Allows function attribute `#[coverage(on/off)]`, to control coverage /// instrumentation of that function. (active, coverage_attribute, "CURRENT_RUSTC_VERSION", Some(84605), None), + /// Allows users to provide classes for fenced code block using `class:classname`. + (active, custom_code_classes_in_docs, "CURRENT_RUSTC_VERSION", Some(79483), None), /// Allows non-builtin attributes in inner attribute position. (active, custom_inner_attributes, "1.30.0", Some(54726), None), /// Allows custom test frameworks with `#![test_runner]` and `#[test_case]`. diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 448314cd9e113..68ce64bc8c026 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -592,6 +592,7 @@ symbols! { cttz, cttz_nonzero, custom_attribute, + custom_code_classes_in_docs, custom_derive, custom_inner_attributes, custom_mir, diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index 039e8cdb98738..d8e36139a7804 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -52,8 +52,9 @@ pub(crate) fn render_example_with_highlighting( out: &mut Buffer, tooltip: Tooltip, playground_button: Option<&str>, + extra_classes: &[String], ) { - write_header(out, "rust-example-rendered", None, tooltip); + write_header(out, "rust-example-rendered", None, tooltip, extra_classes); write_code(out, src, None, None); write_footer(out, playground_button); } @@ -65,7 +66,13 @@ pub(crate) fn render_item_decl_with_highlighting(src: &str, out: &mut Buffer) { write!(out, ""); } -fn write_header(out: &mut Buffer, class: &str, extra_content: Option, tooltip: Tooltip) { +fn write_header( + out: &mut Buffer, + class: &str, + extra_content: Option, + tooltip: Tooltip, + extra_classes: &[String], +) { write!( out, "
", @@ -100,9 +107,19 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option, to out.push_buffer(extra); } if class.is_empty() { - write!(out, "
");
+        write!(
+            out,
+            "
",
+            if extra_classes.is_empty() { "" } else { " " },
+            extra_classes.join(" "),
+        );
     } else {
-        write!(out, "
");
+        write!(
+            out,
+            "
",
+            if extra_classes.is_empty() { "" } else { " " },
+            extra_classes.join(" "),
+        );
     }
     write!(out, "");
 }
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index b28019e3f91b1..a25a6f7d35d10 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -37,8 +37,9 @@ use once_cell::sync::Lazy;
 use std::borrow::Cow;
 use std::collections::VecDeque;
 use std::fmt::Write;
+use std::iter::Peekable;
 use std::ops::{ControlFlow, Range};
-use std::str;
+use std::str::{self, CharIndices};
 
 use crate::clean::RenderedLink;
 use crate::doctest;
@@ -243,11 +244,21 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> {
                 let parse_result =
                     LangString::parse_without_check(lang, self.check_error_codes, false);
                 if !parse_result.rust {
+                    let added_classes = parse_result.added_classes;
+                    let lang_string = if let Some(lang) = parse_result.unknown.first() {
+                        format!("language-{}", lang)
+                    } else {
+                        String::new()
+                    };
+                    let whitespace = if added_classes.is_empty() { "" } else { " " };
                     return Some(Event::Html(
                         format!(
                             "
\ -
{text}
\ +
\
+                                     {text}\
+                                 
\
", + added_classes = added_classes.join(" "), text = Escape(&original_text), ) .into(), @@ -258,6 +269,7 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { CodeBlockKind::Indented => Default::default(), }; + let added_classes = parse_result.added_classes; let lines = original_text.lines().filter_map(|l| map_line(l).for_html()); let text = lines.intersperse("\n".into()).collect::(); @@ -315,6 +327,7 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { &mut s, tooltip, playground_button.as_deref(), + &added_classes, ); Some(Event::Html(s.into_inner().into())) } @@ -711,6 +724,17 @@ pub(crate) fn find_testable_code( error_codes: ErrorCodes, enable_per_target_ignores: bool, extra_info: Option<&ExtraInfo<'_>>, +) { + find_codes(doc, tests, error_codes, enable_per_target_ignores, extra_info, false) +} + +pub(crate) fn find_codes( + doc: &str, + tests: &mut T, + error_codes: ErrorCodes, + enable_per_target_ignores: bool, + extra_info: Option<&ExtraInfo<'_>>, + include_non_rust: bool, ) { let mut parser = Parser::new(doc).into_offset_iter(); let mut prev_offset = 0; @@ -734,7 +758,7 @@ pub(crate) fn find_testable_code( } CodeBlockKind::Indented => Default::default(), }; - if !block_info.rust { + if !include_non_rust && !block_info.rust { continue; } @@ -784,7 +808,19 @@ impl<'tcx> ExtraInfo<'tcx> { ExtraInfo { def_id, sp, tcx } } - fn error_invalid_codeblock_attr(&self, msg: String, help: &'static str) { + fn error_invalid_codeblock_attr(&self, msg: &str) { + if let Some(def_id) = self.def_id.as_local() { + self.tcx.struct_span_lint_hir( + crate::lint::INVALID_CODEBLOCK_ATTRIBUTES, + self.tcx.hir().local_def_id_to_hir_id(def_id), + self.sp, + msg, + |l| l, + ); + } + } + + fn error_invalid_codeblock_attr_with_help(&self, msg: &str, help: &str) { if let Some(def_id) = self.def_id.as_local() { self.tcx.struct_span_lint_hir( crate::lint::INVALID_CODEBLOCK_ATTRIBUTES, @@ -808,6 +844,8 @@ pub(crate) struct LangString { pub(crate) compile_fail: bool, pub(crate) error_codes: Vec, pub(crate) edition: Option, + pub(crate) added_classes: Vec, + pub(crate) unknown: Vec, } #[derive(Eq, PartialEq, Clone, Debug)] @@ -817,6 +855,109 @@ pub(crate) enum Ignore { Some(Vec), } +pub(crate) struct TagIterator<'a, 'tcx> { + inner: Peekable>, + data: &'a str, + is_in_attribute_block: bool, + extra: Option<&'a ExtraInfo<'tcx>>, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum TokenKind<'a> { + Token(&'a str), + Attribute(&'a str), +} + +fn is_separator(c: char) -> bool { + c == ' ' || c == ',' || c == '\t' +} + +impl<'a, 'tcx> TagIterator<'a, 'tcx> { + pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'tcx>>) -> Self { + Self { inner: data.char_indices().peekable(), data, extra, is_in_attribute_block: false } + } + + fn skip_separators(&mut self) -> Option { + while let Some((pos, c)) = self.inner.peek() { + if !is_separator(*c) { + return Some(*pos); + } + self.inner.next(); + } + None + } + + fn emit_error(&self, err: &str) { + if let Some(extra) = self.extra { + extra.error_invalid_codeblock_attr(err); + } + } +} + +impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> { + type Item = TokenKind<'a>; + + fn next(&mut self) -> Option { + let Some(start) = self.skip_separators() else { + if self.is_in_attribute_block { + self.emit_error("unclosed attribute block (`{}`): missing `}` at the end"); + } + return None; + }; + if self.is_in_attribute_block { + while let Some((pos, c)) = self.inner.next() { + if is_separator(c) { + return Some(TokenKind::Attribute(&self.data[start..pos])); + } else if c == '{' { + // There shouldn't be a nested block! + self.emit_error("unexpected `{` inside attribute block (`{}`)"); + let attr = &self.data[start..pos]; + if attr.is_empty() { + return self.next(); + } + self.inner.next(); + return Some(TokenKind::Attribute(attr)); + } else if c == '}' { + self.is_in_attribute_block = false; + let attr = &self.data[start..pos]; + if attr.is_empty() { + return self.next(); + } + return Some(TokenKind::Attribute(attr)); + } + } + // Unclosed attribute block! + self.emit_error("unclosed attribute block (`{}`): missing `}` at the end"); + let token = &self.data[start..]; + if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) } + } else { + while let Some((pos, c)) = self.inner.next() { + if is_separator(c) { + return Some(TokenKind::Token(&self.data[start..pos])); + } else if c == '{' { + self.is_in_attribute_block = true; + let token = &self.data[start..pos]; + if token.is_empty() { + return self.next(); + } + return Some(TokenKind::Token(token)); + } else if c == '}' { + // We're not in a block so it shouldn't be there! + self.emit_error("unexpected `}` outside attribute block (`{}`)"); + let token = &self.data[start..pos]; + if token.is_empty() { + return self.next(); + } + self.inner.next(); + return Some(TokenKind::Attribute(token)); + } + } + let token = &self.data[start..]; + if token.is_empty() { None } else { Some(TokenKind::Token(token)) } + } + } +} + impl Default for LangString { fn default() -> Self { Self { @@ -829,50 +970,37 @@ impl Default for LangString { compile_fail: false, error_codes: Vec::new(), edition: None, + added_classes: Vec::new(), + unknown: Vec::new(), } } } +fn handle_class(class: &str, after: &str, data: &mut LangString, extra: Option<&ExtraInfo<'_>>) { + if class.is_empty() { + if let Some(extra) = extra { + extra.error_invalid_codeblock_attr(&format!("missing class name after `{after}`")); + } + } else { + data.added_classes.push(class.to_owned()); + } +} + impl LangString { fn parse_without_check( string: &str, allow_error_code_check: ErrorCodes, enable_per_target_ignores: bool, - ) -> LangString { + ) -> Self { Self::parse(string, allow_error_code_check, enable_per_target_ignores, None) } - fn tokens(string: &str) -> impl Iterator { - // Pandoc, which Rust once used for generating documentation, - // expects lang strings to be surrounded by `{}` and for each token - // to be proceeded by a `.`. Since some of these lang strings are still - // loose in the wild, we strip a pair of surrounding `{}` from the lang - // string and a leading `.` from each token. - - let string = string.trim(); - - let first = string.chars().next(); - let last = string.chars().last(); - - let string = if first == Some('{') && last == Some('}') { - &string[1..string.len() - 1] - } else { - string - }; - - string - .split(|c| c == ',' || c == ' ' || c == '\t') - .map(str::trim) - .map(|token| token.strip_prefix('.').unwrap_or(token)) - .filter(|token| !token.is_empty()) - } - fn parse( string: &str, allow_error_code_check: ErrorCodes, enable_per_target_ignores: bool, extra: Option<&ExtraInfo<'_>>, - ) -> LangString { + ) -> Self { let allow_error_code_check = allow_error_code_check.as_bool(); let mut seen_rust_tags = false; let mut seen_other_tags = false; @@ -881,43 +1009,45 @@ impl LangString { data.original = string.to_owned(); - for token in Self::tokens(string) { + for token in TagIterator::new(string, extra) { match token { - "should_panic" => { + TokenKind::Token("should_panic") => { data.should_panic = true; seen_rust_tags = !seen_other_tags; } - "no_run" => { + TokenKind::Token("no_run") => { data.no_run = true; seen_rust_tags = !seen_other_tags; } - "ignore" => { + TokenKind::Token("ignore") => { data.ignore = Ignore::All; seen_rust_tags = !seen_other_tags; } - x if x.starts_with("ignore-") => { + TokenKind::Token(x) if x.starts_with("ignore-") => { if enable_per_target_ignores { ignores.push(x.trim_start_matches("ignore-").to_owned()); seen_rust_tags = !seen_other_tags; } } - "rust" => { + TokenKind::Token("rust") => { data.rust = true; seen_rust_tags = true; } - "test_harness" => { + TokenKind::Token("test_harness") => { data.test_harness = true; seen_rust_tags = !seen_other_tags || seen_rust_tags; } - "compile_fail" => { + TokenKind::Token("compile_fail") => { data.compile_fail = true; seen_rust_tags = !seen_other_tags || seen_rust_tags; data.no_run = true; } - x if x.starts_with("edition") => { + TokenKind::Token(x) if x.starts_with("edition") => { data.edition = x[7..].parse::().ok(); } - x if allow_error_code_check && x.starts_with('E') && x.len() == 5 => { + TokenKind::Token(x) + if allow_error_code_check && x.starts_with('E') && x.len() == 5 => + { if x[1..].parse::().is_ok() { data.error_codes.push(x.to_owned()); seen_rust_tags = !seen_other_tags || seen_rust_tags; @@ -925,7 +1055,7 @@ impl LangString { seen_other_tags = true; } } - x if extra.is_some() => { + TokenKind::Token(x) if extra.is_some() => { let s = x.to_lowercase(); if let Some((flag, help)) = if s == "compile-fail" || s == "compile_fail" @@ -958,15 +1088,31 @@ impl LangString { None } { if let Some(extra) = extra { - extra.error_invalid_codeblock_attr( - format!("unknown attribute `{x}`. Did you mean `{flag}`?"), + extra.error_invalid_codeblock_attr_with_help( + &format!("unknown attribute `{}`. Did you mean `{}`?", x, flag), help, ); } } seen_other_tags = true; + data.unknown.push(x.to_owned()); + } + TokenKind::Token(x) => { + seen_other_tags = true; + data.unknown.push(x.to_owned()); + } + TokenKind::Attribute(attr) => { + seen_other_tags = true; + if let Some(class) = attr.strip_prefix('.') { + handle_class(class, ".", &mut data, extra); + } else if let Some(class) = attr.strip_prefix("class=") { + handle_class(class, "class=", &mut data, extra); + } else if let Some(extra) = extra { + extra.error_invalid_codeblock_attr(&format!( + "unsupported attribute `{attr}`" + )); + } } - _ => seen_other_tags = true, } } diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index db8504d15c753..2c9c95590acc5 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -117,6 +117,30 @@ fn test_lang_string_parse() { edition: Some(Edition::Edition2018), ..Default::default() }); + t(LangString { + original: "class:test".into(), + added_classes: vec!["test".into()], + rust: false, + ..Default::default() + }); + t(LangString { + original: "rust,class:test".into(), + added_classes: vec!["test".into()], + rust: true, + ..Default::default() + }); + t(LangString { + original: "class:test:with:colon".into(), + added_classes: vec!["test:with:colon".into()], + rust: false, + ..Default::default() + }); + t(LangString { + original: "class:first,class:second".into(), + added_classes: vec!["first".into(), "second".into()], + rust: false, + ..Default::default() + }); } #[test] diff --git a/src/librustdoc/passes/check_custom_code_classes.rs b/src/librustdoc/passes/check_custom_code_classes.rs new file mode 100644 index 0000000000000..246e7f8f3316b --- /dev/null +++ b/src/librustdoc/passes/check_custom_code_classes.rs @@ -0,0 +1,77 @@ +//! NIGHTLY & UNSTABLE CHECK: custom_code_classes_in_docs +//! +//! This pass will produce errors when finding custom classes outside of +//! nightly + relevant feature active. + +use super::Pass; +use crate::clean::{Crate, Item}; +use crate::core::DocContext; +use crate::fold::DocFolder; +use crate::html::markdown::{find_codes, ErrorCodes, LangString}; + +use rustc_session::parse::feature_err; +use rustc_span::symbol::sym; + +pub(crate) const CHECK_CUSTOM_CODE_CLASSES: Pass = Pass { + name: "check-custom-code-classes", + run: check_custom_code_classes, + description: "check for custom code classes without the feature-gate enabled", +}; + +pub(crate) fn check_custom_code_classes(krate: Crate, cx: &mut DocContext<'_>) -> Crate { + let mut coll = CustomCodeClassLinter { cx }; + + coll.fold_crate(krate) +} + +struct CustomCodeClassLinter<'a, 'tcx> { + cx: &'a DocContext<'tcx>, +} + +impl<'a, 'tcx> DocFolder for CustomCodeClassLinter<'a, 'tcx> { + fn fold_item(&mut self, item: Item) -> Option { + look_for_custom_classes(&self.cx, &item); + Some(self.fold_item_recur(item)) + } +} + +#[derive(Debug)] +struct TestsWithCustomClasses { + custom_classes_found: Vec, +} + +impl crate::doctest::Tester for TestsWithCustomClasses { + fn add_test(&mut self, _: String, config: LangString, _: usize) { + self.custom_classes_found.extend(config.added_classes.into_iter()); + } +} + +pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item) { + if !item.item_id.is_local() { + // If non-local, no need to check anything. + return; + } + + let mut tests = TestsWithCustomClasses { custom_classes_found: vec![] }; + + let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); + find_codes(&dox, &mut tests, ErrorCodes::No, false, None, true); + + if !tests.custom_classes_found.is_empty() && !cx.tcx.features().custom_code_classes_in_docs { + feature_err( + &cx.tcx.sess.parse_sess, + sym::custom_code_classes_in_docs, + item.attr_span(cx.tcx), + "custom classes in code blocks are unstable", + ) + .note( + // This will list the wrong items to make them more easily searchable. + // To ensure the most correct hits, it adds back the 'class:' that was stripped. + &format!( + "found these custom classes: class={}", + tests.custom_classes_found.join(",class=") + ), + ) + .emit(); + } +} diff --git a/src/librustdoc/passes/mod.rs b/src/librustdoc/passes/mod.rs index bb678e3388880..4eeaaa2bb70a9 100644 --- a/src/librustdoc/passes/mod.rs +++ b/src/librustdoc/passes/mod.rs @@ -35,6 +35,9 @@ pub(crate) use self::calculate_doc_coverage::CALCULATE_DOC_COVERAGE; mod lint; pub(crate) use self::lint::RUN_LINTS; +mod check_custom_code_classes; +pub(crate) use self::check_custom_code_classes::CHECK_CUSTOM_CODE_CLASSES; + /// A single pass over the cleaned documentation. /// /// Runs in the compiler context, so it has access to types and traits and the like. @@ -66,6 +69,7 @@ pub(crate) enum Condition { /// The full list of passes. pub(crate) const PASSES: &[Pass] = &[ + CHECK_CUSTOM_CODE_CLASSES, CHECK_DOC_TEST_VISIBILITY, STRIP_HIDDEN, STRIP_PRIVATE, @@ -79,6 +83,7 @@ pub(crate) const PASSES: &[Pass] = &[ /// The list of passes run by default. pub(crate) const DEFAULT_PASSES: &[ConditionalPass] = &[ + ConditionalPass::always(CHECK_CUSTOM_CODE_CLASSES), ConditionalPass::always(COLLECT_TRAIT_IMPLS), ConditionalPass::always(CHECK_DOC_TEST_VISIBILITY), ConditionalPass::new(STRIP_HIDDEN, WhenNotDocumentHidden), From f5561842e3dfc9adc8da4ba12b95514da4d99f00 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 25 Apr 2023 15:04:46 +0200 Subject: [PATCH 02/13] Add tests for `custom_code_classes_in_docs` feature --- src/librustdoc/html/markdown/tests.rs | 131 ++++++++++++++---- .../custom_code_classes_in_docs-warning.rs | 19 +++ ...custom_code_classes_in_docs-warning.stderr | 65 +++++++++ .../custom_code_classes_in_docs-warning2.rs | 13 ++ ...ustom_code_classes_in_docs-warning2.stderr | 17 +++ ...eature-gate-custom_code_classes_in_docs.rs | 5 + ...re-gate-custom_code_classes_in_docs.stderr | 15 ++ tests/rustdoc-ui/issues/issue-91713.stdout | 2 + tests/rustdoc/custom_code_classes.rs | 28 ++++ 9 files changed, 268 insertions(+), 27 deletions(-) create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr create mode 100644 tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs create mode 100644 tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr create mode 100644 tests/rustdoc/custom_code_classes.rs diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index 2c9c95590acc5..dd3d0ebac0cd4 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -1,5 +1,8 @@ use super::{find_testable_code, plain_text_summary, short_markdown_summary}; -use super::{ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownItemInfo}; +use super::{ + ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownItemInfo, TagIterator, + TokenKind, +}; use rustc_span::edition::{Edition, DEFAULT_EDITION}; #[test] @@ -51,10 +54,25 @@ fn test_lang_string_parse() { t(Default::default()); t(LangString { original: "rust".into(), ..Default::default() }); - t(LangString { original: ".rust".into(), ..Default::default() }); - t(LangString { original: "{rust}".into(), ..Default::default() }); - t(LangString { original: "{.rust}".into(), ..Default::default() }); - t(LangString { original: "sh".into(), rust: false, ..Default::default() }); + t(LangString { + original: ".rust".into(), + rust: false, + unknown: vec![".rust".into()], + ..Default::default() + }); + t(LangString { original: "{rust}".into(), rust: false, ..Default::default() }); + t(LangString { + original: "{.rust}".into(), + rust: false, + added_classes: vec!["rust".into()], + ..Default::default() + }); + t(LangString { + original: "sh".into(), + rust: false, + unknown: vec!["sh".into()], + ..Default::default() + }); t(LangString { original: "ignore".into(), ignore: Ignore::All, ..Default::default() }); t(LangString { original: "ignore-foo".into(), @@ -70,41 +88,56 @@ fn test_lang_string_parse() { compile_fail: true, ..Default::default() }); - t(LangString { original: "no_run,example".into(), no_run: true, ..Default::default() }); + t(LangString { + original: "no_run,example".into(), + no_run: true, + unknown: vec!["example".into()], + ..Default::default() + }); t(LangString { original: "sh,should_panic".into(), should_panic: true, rust: false, + unknown: vec!["sh".into()], + ..Default::default() + }); + t(LangString { + original: "example,rust".into(), + unknown: vec!["example".into()], ..Default::default() }); - t(LangString { original: "example,rust".into(), ..Default::default() }); t(LangString { original: "test_harness,.rust".into(), test_harness: true, + unknown: vec![".rust".into()], ..Default::default() }); t(LangString { original: "text, no_run".into(), no_run: true, rust: false, + unknown: vec!["text".into()], ..Default::default() }); t(LangString { original: "text,no_run".into(), no_run: true, rust: false, + unknown: vec!["text".into()], ..Default::default() }); t(LangString { original: "text,no_run, ".into(), no_run: true, rust: false, + unknown: vec!["text".into()], ..Default::default() }); t(LangString { original: "text,no_run,".into(), no_run: true, rust: false, + unknown: vec!["text".into()], ..Default::default() }); t(LangString { @@ -118,52 +151,96 @@ fn test_lang_string_parse() { ..Default::default() }); t(LangString { - original: "class:test".into(), + original: "{class=test}".into(), added_classes: vec!["test".into()], rust: false, ..Default::default() }); t(LangString { - original: "rust,class:test".into(), + original: "{.test}".into(), added_classes: vec!["test".into()], + rust: false, + ..Default::default() + }); + t(LangString { + original: "rust,{class=test,.test2}".into(), + added_classes: vec!["test".into(), "test2".into()], rust: true, ..Default::default() }); t(LangString { - original: "class:test:with:colon".into(), - added_classes: vec!["test:with:colon".into()], + original: "{class=test:with:colon .test1}".into(), + added_classes: vec!["test:with:colon".into(), "test1".into()], rust: false, ..Default::default() }); t(LangString { - original: "class:first,class:second".into(), + original: "{class=first,class=second}".into(), added_classes: vec!["first".into(), "second".into()], rust: false, ..Default::default() }); + t(LangString { + original: "{class=first,.second},unknown".into(), + added_classes: vec!["first".into(), "second".into()], + rust: false, + unknown: vec!["unknown".into()], + ..Default::default() + }); + t(LangString { + original: "{class=first .second} unknown".into(), + added_classes: vec!["first".into(), "second".into()], + rust: false, + unknown: vec!["unknown".into()], + ..Default::default() + }); + t(LangString { + original: "{.first.second}".into(), + added_classes: vec!["first.second".into()], + rust: false, + ..Default::default() + }); + t(LangString { + original: "{class=first=second}".into(), + added_classes: vec!["first=second".into()], + rust: false, + ..Default::default() + }); + t(LangString { + original: "{class=first.second}".into(), + added_classes: vec!["first.second".into()], + rust: false, + ..Default::default() + }); + t(LangString { + original: "{class=.first}".into(), + added_classes: vec![".first".into()], + rust: false, + ..Default::default() + }); } #[test] fn test_lang_string_tokenizer() { - fn case(lang_string: &str, want: &[&str]) { - let have = LangString::tokens(lang_string).collect::>(); + fn case(lang_string: &str, want: &[TokenKind<'_>]) { + let have = TagIterator::new(lang_string, None).collect::>(); assert_eq!(have, want, "Unexpected lang string split for `{}`", lang_string); } case("", &[]); - case("foo", &["foo"]); - case("foo,bar", &["foo", "bar"]); - case(".foo,.bar", &["foo", "bar"]); - case("{.foo,.bar}", &["foo", "bar"]); - case(" {.foo,.bar} ", &["foo", "bar"]); - case("foo bar", &["foo", "bar"]); - case("foo\tbar", &["foo", "bar"]); - case("foo\t, bar", &["foo", "bar"]); - case(" foo , bar ", &["foo", "bar"]); - case(",,foo,,bar,,", &["foo", "bar"]); - case("foo=bar", &["foo=bar"]); - case("a-b-c", &["a-b-c"]); - case("a_b_c", &["a_b_c"]); + case("foo", &[TokenKind::Token("foo")]); + case("foo,bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); + case(".foo,.bar", &[TokenKind::Token(".foo"), TokenKind::Token(".bar")]); + case("{.foo,.bar}", &[TokenKind::Attribute(".foo"), TokenKind::Attribute(".bar")]); + case(" {.foo,.bar} ", &[TokenKind::Attribute(".foo"), TokenKind::Attribute(".bar")]); + case("foo bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); + case("foo\tbar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); + case("foo\t, bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); + case(" foo , bar ", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); + case(",,foo,,bar,,", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); + case("foo=bar", &[TokenKind::Token("foo=bar")]); + case("a-b-c", &[TokenKind::Token("a-b-c")]); + case("a_b_c", &[TokenKind::Token("a_b_c")]); } #[test] diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs new file mode 100644 index 0000000000000..c28921b01f133 --- /dev/null +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs @@ -0,0 +1,19 @@ +// This test ensures that warnings are working as expected for "custom_code_classes_in_docs" +// feature. + +#![feature(custom_code_classes_in_docs)] +#![deny(warnings)] +#![feature(no_core)] +#![no_core] + +/// ```{. class= whatever=hehe #id} } {{ +/// main; +/// ``` +//~^^^ ERROR missing class name after `.` +//~| ERROR missing class name after `class=` +//~| ERROR unsupported attribute `whatever=hehe` +//~| ERROR unsupported attribute `#id` +//~| ERROR unexpected `}` outside attribute block (`{}`) +//~| ERROR unclosed attribute block (`{}`): missing `}` at the end +//~| ERROR unexpected `{` inside attribute block (`{}`) +pub fn foo() {} diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr new file mode 100644 index 0000000000000..f19b62914dbfb --- /dev/null +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr @@ -0,0 +1,65 @@ +error: missing class name after `.` + --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 + | +LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | | /// main; +LL | | /// ``` + | |_______^ + | +note: the lint level is defined here + --> $DIR/custom_code_classes_in_docs-warning.rs:5:9 + | +LL | #![deny(warnings)] + | ^^^^^^^^ + = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]` + +error: missing class name after `class=` + --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 + | +LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unsupported attribute `whatever=hehe` + --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 + | +LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unsupported attribute `#id` + --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 + | +LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unexpected `}` outside attribute block (`{}`) + --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 + | +LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unexpected `{` inside attribute block (`{}`) + --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 + | +LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unclosed attribute block (`{}`): missing `}` at the end + --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 + | +LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: aborting due to 7 previous errors + diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs new file mode 100644 index 0000000000000..b2ce7407ec6d8 --- /dev/null +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs @@ -0,0 +1,13 @@ +// This test ensures that warnings are working as expected for "custom_code_classes_in_docs" +// feature. + +#![feature(custom_code_classes_in_docs)] +#![deny(warnings)] +#![feature(no_core)] +#![no_core] + +/// ```{class=} +/// main; +/// ``` +//~^^^ ERROR missing class name after `class=` +pub fn foo() {} diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr new file mode 100644 index 0000000000000..52bb1dae9f61f --- /dev/null +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr @@ -0,0 +1,17 @@ +error: missing class name after `class=` + --> $DIR/custom_code_classes_in_docs-warning2.rs:9:1 + | +LL | / /// ```{class=} +LL | | /// main; +LL | | /// ``` + | |_______^ + | +note: the lint level is defined here + --> $DIR/custom_code_classes_in_docs-warning2.rs:5:9 + | +LL | #![deny(warnings)] + | ^^^^^^^^ + = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]` + +error: aborting due to previous error + diff --git a/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs b/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs new file mode 100644 index 0000000000000..8aa13b2d5d10c --- /dev/null +++ b/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs @@ -0,0 +1,5 @@ +/// ```{class=language-c} +/// int main(void) { return 0; } +/// ``` +//~^^^ ERROR 1:1: 3:8: custom classes in code blocks are unstable [E0658] +pub struct Bar; diff --git a/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr b/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr new file mode 100644 index 0000000000000..c41ebfc807334 --- /dev/null +++ b/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr @@ -0,0 +1,15 @@ +error[E0658]: custom classes in code blocks are unstable + --> $DIR/feature-gate-custom_code_classes_in_docs.rs:1:1 + | +LL | / /// ```{class=language-c} +LL | | /// int main(void) { return 0; } +LL | | /// ``` + | |_______^ + | + = note: see issue #79483 for more information + = help: add `#![feature(custom_code_classes_in_docs)]` to the crate attributes to enable + = note: found these custom classes: class=language-c + +error: aborting due to previous error + +For more information about this error, try `rustc --explain E0658`. diff --git a/tests/rustdoc-ui/issues/issue-91713.stdout b/tests/rustdoc-ui/issues/issue-91713.stdout index 1678352436315..bbea7e5c212ef 100644 --- a/tests/rustdoc-ui/issues/issue-91713.stdout +++ b/tests/rustdoc-ui/issues/issue-91713.stdout @@ -1,4 +1,5 @@ Available passes for running rustdoc: +check-custom-code-classes - check for custom code classes without the feature-gate enabled check_doc_test_visibility - run various visibility-related lints on doctests strip-hidden - strips all `#[doc(hidden)]` items from the output strip-private - strips all private items from a crate which cannot be seen externally, implies strip-priv-imports @@ -10,6 +11,7 @@ calculate-doc-coverage - counts the number of items with and without documentati run-lints - runs some of rustdoc's lints Default passes for rustdoc: +check-custom-code-classes collect-trait-impls check_doc_test_visibility strip-hidden (when not --document-hidden-items) diff --git a/tests/rustdoc/custom_code_classes.rs b/tests/rustdoc/custom_code_classes.rs new file mode 100644 index 0000000000000..f110721c5a715 --- /dev/null +++ b/tests/rustdoc/custom_code_classes.rs @@ -0,0 +1,28 @@ +// Test for `custom_code_classes_in_docs` feature. + +#![feature(custom_code_classes_in_docs)] +#![crate_name = "foo"] +#![feature(no_core)] +#![no_core] + +// @has 'foo/struct.Bar.html' +// @has - '//*[@id="main-content"]//pre[@class="language-whatever hoho-c"]' 'main;' +// @has - '//*[@id="main-content"]//pre[@class="language-whatever2 haha-c"]' 'main;' +// @has - '//*[@id="main-content"]//pre[@class="language-whatever4 huhu-c"]' 'main;' + +/// ```{class=hoho-c},whatever +/// main; +/// ``` +/// +/// Testing multiple kinds of orders. +/// +/// ```whatever2 {class=haha-c} +/// main; +/// ``` +/// +/// Testing with multiple "unknown". Only the first should be used. +/// +/// ```whatever4{.huhu-c} whatever5 +/// main; +/// ``` +pub struct Bar; From d829fee6b5859de516dadaaba30db758bb567268 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 25 Apr 2023 16:37:43 +0200 Subject: [PATCH 03/13] Add documentation for `custom_code_classes_in_docs` feature --- src/doc/rustdoc/src/unstable-features.md | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index f69156b7c05e2..bb62a0bc9cc83 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -625,3 +625,32 @@ and check the values of `feature`: `foo` and `bar`. This flag enables the generation of links in the source code pages which allow the reader to jump to a type definition. + +### Custom CSS classes for code blocks + +```rust +#![feature(custom_code_classes_in_docs)] + +/// ```{class=language-c} +/// int main(void) { return 0; } +/// ``` +pub struct Bar; +``` + +The text `int main(void) { return 0; }` is rendered without highlighting in a code block +with the class `language-c`. This can be used to highlight other languages through JavaScript +libraries for example. + +To be noted that you can replace `class=` with `.` to achieve the same result: + +```rust +#![feature(custom_code_classes_in_docs)] + +/// ```{.language-c} +/// int main(void) { return 0; } +/// ``` +pub struct Bar; +``` + +To be noted, `rust` and `.rust`/`class=rust` have different effects: `rust` indicates that this is +a Rust code block whereas the two others add a "rust" CSS class on the code block. From 4ce17fa30eeef32235e9b305f56b5651c6d35276 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 27 Apr 2023 15:09:43 +0200 Subject: [PATCH 04/13] Add support for double quotes in markdown codeblock attributes --- src/doc/rustdoc/src/unstable-features.md | 11 ++ src/librustdoc/html/markdown.rs | 121 +++++++++++------- src/librustdoc/html/markdown/tests.rs | 12 ++ .../custom_code_classes_in_docs-warning3.rs | 17 +++ ...ustom_code_classes_in_docs-warning3.stderr | 33 +++++ 5 files changed, 145 insertions(+), 49 deletions(-) create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning3.rs create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index bb62a0bc9cc83..b5d8c8194188a 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -654,3 +654,14 @@ pub struct Bar; To be noted, `rust` and `.rust`/`class=rust` have different effects: `rust` indicates that this is a Rust code block whereas the two others add a "rust" CSS class on the code block. + +You can also use double quotes: + +```rust +#![feature(custom_code_classes_in_docs)] + +/// ```"not rust" {."hello everyone"} +/// int main(void) { return 0; } +/// ``` +pub struct Bar; +``` diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index a25a6f7d35d10..6bd4e775c0e76 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -892,6 +892,75 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { extra.error_invalid_codeblock_attr(err); } } + + /// Returns false if the string is unfinished. + fn skip_string(&mut self) -> bool { + while let Some((_, c)) = self.inner.next() { + if c == '"' { + return true; + } + } + self.emit_error("unclosed quote string: missing `\"` at the end"); + false + } + + fn parse_in_attribute_block(&mut self, start: usize) -> Option> { + while let Some((pos, c)) = self.inner.next() { + if is_separator(c) { + return Some(TokenKind::Attribute(&self.data[start..pos])); + } else if c == '{' { + // There shouldn't be a nested block! + self.emit_error("unexpected `{` inside attribute block (`{}`)"); + let attr = &self.data[start..pos]; + if attr.is_empty() { + return self.next(); + } + self.inner.next(); + return Some(TokenKind::Attribute(attr)); + } else if c == '}' { + self.is_in_attribute_block = false; + let attr = &self.data[start..pos]; + if attr.is_empty() { + return self.next(); + } + return Some(TokenKind::Attribute(attr)); + } else if c == '"' && !self.skip_string() { + return None; + } + } + // Unclosed attribute block! + self.emit_error("unclosed attribute block (`{}`): missing `}` at the end"); + let token = &self.data[start..]; + if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) } + } + + fn parse_outside_attribute_block(&mut self, start: usize) -> Option> { + while let Some((pos, c)) = self.inner.next() { + if is_separator(c) { + return Some(TokenKind::Token(&self.data[start..pos])); + } else if c == '{' { + self.is_in_attribute_block = true; + let token = &self.data[start..pos]; + if token.is_empty() { + return self.next(); + } + return Some(TokenKind::Token(token)); + } else if c == '}' { + // We're not in a block so it shouldn't be there! + self.emit_error("unexpected `}` outside attribute block (`{}`)"); + let token = &self.data[start..pos]; + if token.is_empty() { + return self.next(); + } + self.inner.next(); + return Some(TokenKind::Attribute(token)); + } else if c == '"' && !self.skip_string() { + return None; + } + } + let token = &self.data[start..]; + if token.is_empty() { None } else { Some(TokenKind::Token(token)) } + } } impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> { @@ -905,55 +974,9 @@ impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> { return None; }; if self.is_in_attribute_block { - while let Some((pos, c)) = self.inner.next() { - if is_separator(c) { - return Some(TokenKind::Attribute(&self.data[start..pos])); - } else if c == '{' { - // There shouldn't be a nested block! - self.emit_error("unexpected `{` inside attribute block (`{}`)"); - let attr = &self.data[start..pos]; - if attr.is_empty() { - return self.next(); - } - self.inner.next(); - return Some(TokenKind::Attribute(attr)); - } else if c == '}' { - self.is_in_attribute_block = false; - let attr = &self.data[start..pos]; - if attr.is_empty() { - return self.next(); - } - return Some(TokenKind::Attribute(attr)); - } - } - // Unclosed attribute block! - self.emit_error("unclosed attribute block (`{}`): missing `}` at the end"); - let token = &self.data[start..]; - if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) } + self.parse_in_attribute_block(start) } else { - while let Some((pos, c)) = self.inner.next() { - if is_separator(c) { - return Some(TokenKind::Token(&self.data[start..pos])); - } else if c == '{' { - self.is_in_attribute_block = true; - let token = &self.data[start..pos]; - if token.is_empty() { - return self.next(); - } - return Some(TokenKind::Token(token)); - } else if c == '}' { - // We're not in a block so it shouldn't be there! - self.emit_error("unexpected `}` outside attribute block (`{}`)"); - let token = &self.data[start..pos]; - if token.is_empty() { - return self.next(); - } - self.inner.next(); - return Some(TokenKind::Attribute(token)); - } - } - let token = &self.data[start..]; - if token.is_empty() { None } else { Some(TokenKind::Token(token)) } + self.parse_outside_attribute_block(start) } } } @@ -982,7 +1005,7 @@ fn handle_class(class: &str, after: &str, data: &mut LangString, extra: Option<& extra.error_invalid_codeblock_attr(&format!("missing class name after `{after}`")); } } else { - data.added_classes.push(class.to_owned()); + data.added_classes.push(class.replace('"', "")); } } diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index dd3d0ebac0cd4..b0b4de65cca40 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -218,6 +218,18 @@ fn test_lang_string_parse() { rust: false, ..Default::default() }); + t(LangString { + original: r#"{class="first"}"#.into(), + added_classes: vec!["first".into()], + rust: false, + ..Default::default() + }); + t(LangString { + original: r#"{class=f"irst"}"#.into(), + added_classes: vec!["first".into()], + rust: false, + ..Default::default() + }); } #[test] diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.rs new file mode 100644 index 0000000000000..57d9038cb0ce1 --- /dev/null +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.rs @@ -0,0 +1,17 @@ +// This test ensures that warnings are working as expected for "custom_code_classes_in_docs" +// feature. + +#![feature(custom_code_classes_in_docs)] +#![deny(warnings)] +#![feature(no_core)] +#![no_core] + +/// ```{class="} +/// main; +/// ``` +//~^^^ ERROR unclosed quote string +//~| ERROR unclosed quote string +/// ```" +/// main; +/// ``` +pub fn foo() {} diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr new file mode 100644 index 0000000000000..7432af19360bc --- /dev/null +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr @@ -0,0 +1,33 @@ +error: unclosed quote string: missing `"` at the end + --> $DIR/custom_code_classes_in_docs-warning3.rs:9:1 + | +LL | / /// ```{class="} +LL | | /// main; +LL | | /// ``` +LL | | +... | +LL | | /// main; +LL | | /// ``` + | |_______^ + | +note: the lint level is defined here + --> $DIR/custom_code_classes_in_docs-warning3.rs:5:9 + | +LL | #![deny(warnings)] + | ^^^^^^^^ + = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]` + +error: unclosed quote string: missing `"` at the end + --> $DIR/custom_code_classes_in_docs-warning3.rs:9:1 + | +LL | / /// ```{class="} +LL | | /// main; +LL | | /// ``` +LL | | +... | +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: aborting due to 2 previous errors + From 7681f63cab88109dd8d445cf7b49cbbd63f81e75 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 2 May 2023 22:01:28 +0200 Subject: [PATCH 05/13] Implement new eBNF for codeblock attributes --- src/librustdoc/html/markdown.rs | 272 ++++++++++++------ src/librustdoc/html/markdown/tests.rs | 89 +++--- .../custom_code_classes_in_docs-warning.rs | 82 +++++- ...custom_code_classes_in_docs-warning.stderr | 90 ++++-- .../custom_code_classes_in_docs-warning2.rs | 13 - ...ustom_code_classes_in_docs-warning2.stderr | 17 -- ...ustom_code_classes_in_docs-warning3.stderr | 4 +- tests/rustdoc/custom_code_classes.rs | 2 +- 8 files changed, 373 insertions(+), 196 deletions(-) delete mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs delete mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 6bd4e775c0e76..0a741f7815b89 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -862,19 +862,34 @@ pub(crate) struct TagIterator<'a, 'tcx> { extra: Option<&'a ExtraInfo<'tcx>>, } -#[derive(Debug, PartialEq)] -pub(crate) enum TokenKind<'a> { - Token(&'a str), - Attribute(&'a str), +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum LangStringToken<'a> { + LangToken(&'a str), + ClassAttribute(&'a str), + KeyValueAttribute(&'a str, &'a str), } +fn is_bareword_char(c: char) -> bool { + c == '_' || c == '-' || c == ':' || c.is_ascii_alphabetic() || c.is_ascii_digit() +} fn is_separator(c: char) -> bool { c == ' ' || c == ',' || c == '\t' } +struct Indices { + start: usize, + end: usize, +} + impl<'a, 'tcx> TagIterator<'a, 'tcx> { pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'tcx>>) -> Self { - Self { inner: data.char_indices().peekable(), data, extra, is_in_attribute_block: false } + Self { inner: data.char_indices().peekable(), data, is_in_attribute_block: false, extra } + } + + fn emit_error(&self, err: &str) { + if let Some(extra) = self.extra { + extra.error_invalid_codeblock_attr(err); + } } fn skip_separators(&mut self) -> Option { @@ -887,84 +902,183 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { None } - fn emit_error(&self, err: &str) { - if let Some(extra) = self.extra { - extra.error_invalid_codeblock_attr(err); + fn parse_string(&mut self, start: usize) -> Option { + while let Some((pos, c)) = self.inner.next() { + if c == '"' { + return Some(Indices { start: start + 1, end: pos }); + } } + self.emit_error("unclosed quote string `\"`"); + None } - /// Returns false if the string is unfinished. - fn skip_string(&mut self) -> bool { - while let Some((_, c)) = self.inner.next() { - if c == '"' { - return true; + fn parse_class(&mut self, start: usize) -> Option> { + while let Some((pos, c)) = self.inner.peek().copied() { + if is_bareword_char(c) { + self.inner.next(); + } else { + let class = &self.data[start + 1..pos]; + if class.is_empty() { + self.emit_error(&format!("unexpected `{c}` character after `.`")); + return None; + } else if self.check_after_token() { + return Some(LangStringToken::ClassAttribute(class)); + } else { + return None; + } } } - self.emit_error("unclosed quote string: missing `\"` at the end"); - false + let class = &self.data[start + 1..]; + if class.is_empty() { + self.emit_error("missing character after `.`"); + None + } else if self.check_after_token() { + Some(LangStringToken::ClassAttribute(class)) + } else { + None + } + } + + fn parse_token(&mut self, start: usize) -> Option { + while let Some((pos, c)) = self.inner.peek() { + if !is_bareword_char(*c) { + return Some(Indices { start, end: *pos }); + } + self.inner.next(); + } + self.emit_error("unexpected end"); + None + } + + fn parse_key_value(&mut self, c: char, start: usize) -> Option> { + let key_indices = + if c == '"' { self.parse_string(start)? } else { self.parse_token(start)? }; + if key_indices.start == key_indices.end { + self.emit_error("unexpected empty string as key"); + return None; + } + + if let Some((_, c)) = self.inner.next() { + if c != '=' { + self.emit_error(&format!("expected `=`, found `{}`", c)); + return None; + } + } else { + self.emit_error("unexpected end"); + return None; + } + let value_indices = match self.inner.next() { + Some((pos, '"')) => self.parse_string(pos)?, + Some((pos, c)) if is_bareword_char(c) => self.parse_token(pos)?, + Some((_, c)) => { + self.emit_error(&format!("unexpected `{c}` character after `=`")); + return None; + } + None => { + self.emit_error("expected value after `=`"); + return None; + } + }; + if value_indices.start == value_indices.end { + self.emit_error("unexpected empty string as value"); + None + } else if self.check_after_token() { + Some(LangStringToken::KeyValueAttribute( + &self.data[key_indices.start..key_indices.end], + &self.data[value_indices.start..value_indices.end], + )) + } else { + None + } } - fn parse_in_attribute_block(&mut self, start: usize) -> Option> { + /// Returns `false` if an error was emitted. + fn check_after_token(&mut self) -> bool { + if let Some((_, c)) = self.inner.peek().copied() { + if c == '}' || is_separator(c) || c == '(' { + true + } else { + self.emit_error(&format!("unexpected `{c}` character")); + false + } + } else { + // The error will be caught on the next iteration. + true + } + } + + fn parse_in_attribute_block(&mut self) -> Option> { while let Some((pos, c)) = self.inner.next() { - if is_separator(c) { - return Some(TokenKind::Attribute(&self.data[start..pos])); - } else if c == '{' { - // There shouldn't be a nested block! - self.emit_error("unexpected `{` inside attribute block (`{}`)"); - let attr = &self.data[start..pos]; - if attr.is_empty() { - return self.next(); - } - self.inner.next(); - return Some(TokenKind::Attribute(attr)); - } else if c == '}' { + if c == '}' { self.is_in_attribute_block = false; - let attr = &self.data[start..pos]; - if attr.is_empty() { - return self.next(); - } - return Some(TokenKind::Attribute(attr)); - } else if c == '"' && !self.skip_string() { + return self.next(); + } else if c == '.' { + return self.parse_class(pos); + } else if c == '"' || is_bareword_char(c) { + return self.parse_key_value(c, pos); + } else { + self.emit_error(&format!("unexpected character `{c}`")); return None; } } - // Unclosed attribute block! self.emit_error("unclosed attribute block (`{}`): missing `}` at the end"); - let token = &self.data[start..]; - if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) } + None } - fn parse_outside_attribute_block(&mut self, start: usize) -> Option> { + /// Returns `false` if an error was emitted. + fn skip_paren_block(&mut self) -> bool { + while let Some((_, c)) = self.inner.next() { + if c == ')' { + return true; + } + } + self.emit_error("unclosed comment: missing `)` at the end"); + false + } + + fn parse_outside_attribute_block(&mut self, start: usize) -> Option> { while let Some((pos, c)) = self.inner.next() { - if is_separator(c) { - return Some(TokenKind::Token(&self.data[start..pos])); + if c == '"' { + if pos != start { + self.emit_error("expected ` `, `{` or `,` found `\"`"); + return None; + } + let indices = self.parse_string(pos)?; + if let Some((_, c)) = self.inner.peek().copied() && c != '{' && !is_separator(c) && c != '(' { + self.emit_error(&format!("expected ` `, `{{` or `,` after `\"`, found `{c}`")); + return None; + } + return Some(LangStringToken::LangToken(&self.data[indices.start..indices.end])); } else if c == '{' { self.is_in_attribute_block = true; - let token = &self.data[start..pos]; - if token.is_empty() { - return self.next(); + return self.next(); + } else if is_bareword_char(c) { + continue; + } else if is_separator(c) { + if pos != start { + return Some(LangStringToken::LangToken(&self.data[start..pos])); } - return Some(TokenKind::Token(token)); - } else if c == '}' { - // We're not in a block so it shouldn't be there! - self.emit_error("unexpected `}` outside attribute block (`{}`)"); - let token = &self.data[start..pos]; - if token.is_empty() { - return self.next(); + return self.next(); + } else if c == '(' { + if !self.skip_paren_block() { + return None; } - self.inner.next(); - return Some(TokenKind::Attribute(token)); - } else if c == '"' && !self.skip_string() { + if pos != start { + return Some(LangStringToken::LangToken(&self.data[start..pos])); + } + return self.next(); + } else { + self.emit_error(&format!("unexpected character `{c}`")); return None; } } let token = &self.data[start..]; - if token.is_empty() { None } else { Some(TokenKind::Token(token)) } + if token.is_empty() { None } else { Some(LangStringToken::LangToken(&self.data[start..])) } } } impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> { - type Item = TokenKind<'a>; + type Item = LangStringToken<'a>; fn next(&mut self) -> Option { let Some(start) = self.skip_separators() else { @@ -974,7 +1088,7 @@ impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> { return None; }; if self.is_in_attribute_block { - self.parse_in_attribute_block(start) + self.parse_in_attribute_block() } else { self.parse_outside_attribute_block(start) } @@ -999,16 +1113,6 @@ impl Default for LangString { } } -fn handle_class(class: &str, after: &str, data: &mut LangString, extra: Option<&ExtraInfo<'_>>) { - if class.is_empty() { - if let Some(extra) = extra { - extra.error_invalid_codeblock_attr(&format!("missing class name after `{after}`")); - } - } else { - data.added_classes.push(class.replace('"', "")); - } -} - impl LangString { fn parse_without_check( string: &str, @@ -1034,41 +1138,41 @@ impl LangString { for token in TagIterator::new(string, extra) { match token { - TokenKind::Token("should_panic") => { + LangStringToken::LangToken("should_panic") => { data.should_panic = true; seen_rust_tags = !seen_other_tags; } - TokenKind::Token("no_run") => { + LangStringToken::LangToken("no_run") => { data.no_run = true; seen_rust_tags = !seen_other_tags; } - TokenKind::Token("ignore") => { + LangStringToken::LangToken("ignore") => { data.ignore = Ignore::All; seen_rust_tags = !seen_other_tags; } - TokenKind::Token(x) if x.starts_with("ignore-") => { + LangStringToken::LangToken(x) if x.starts_with("ignore-") => { if enable_per_target_ignores { ignores.push(x.trim_start_matches("ignore-").to_owned()); seen_rust_tags = !seen_other_tags; } } - TokenKind::Token("rust") => { + LangStringToken::LangToken("rust") => { data.rust = true; seen_rust_tags = true; } - TokenKind::Token("test_harness") => { + LangStringToken::LangToken("test_harness") => { data.test_harness = true; seen_rust_tags = !seen_other_tags || seen_rust_tags; } - TokenKind::Token("compile_fail") => { + LangStringToken::LangToken("compile_fail") => { data.compile_fail = true; seen_rust_tags = !seen_other_tags || seen_rust_tags; data.no_run = true; } - TokenKind::Token(x) if x.starts_with("edition") => { + LangStringToken::LangToken(x) if x.starts_with("edition") => { data.edition = x[7..].parse::().ok(); } - TokenKind::Token(x) + LangStringToken::LangToken(x) if allow_error_code_check && x.starts_with('E') && x.len() == 5 => { if x[1..].parse::().is_ok() { @@ -1078,7 +1182,7 @@ impl LangString { seen_other_tags = true; } } - TokenKind::Token(x) if extra.is_some() => { + LangStringToken::LangToken(x) if extra.is_some() => { let s = x.to_lowercase(); if let Some((flag, help)) = if s == "compile-fail" || s == "compile_fail" @@ -1120,22 +1224,24 @@ impl LangString { seen_other_tags = true; data.unknown.push(x.to_owned()); } - TokenKind::Token(x) => { + LangStringToken::LangToken(x) => { seen_other_tags = true; data.unknown.push(x.to_owned()); } - TokenKind::Attribute(attr) => { + LangStringToken::KeyValueAttribute(key, value) => { seen_other_tags = true; - if let Some(class) = attr.strip_prefix('.') { - handle_class(class, ".", &mut data, extra); - } else if let Some(class) = attr.strip_prefix("class=") { - handle_class(class, "class=", &mut data, extra); + if key == "class" { + data.added_classes.push(value.to_owned()); } else if let Some(extra) = extra { extra.error_invalid_codeblock_attr(&format!( - "unsupported attribute `{attr}`" + "unsupported attribute `{key}`" )); } } + LangStringToken::ClassAttribute(class) => { + seen_other_tags = true; + data.added_classes.push(class.to_owned()); + } } } diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index b0b4de65cca40..35b243d3d292e 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -1,7 +1,7 @@ use super::{find_testable_code, plain_text_summary, short_markdown_summary}; use super::{ - ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownItemInfo, TagIterator, - TokenKind, + ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, LangStringToken, Markdown, + MarkdownItemInfo, TagIterator, }; use rustc_span::edition::{Edition, DEFAULT_EDITION}; @@ -55,12 +55,13 @@ fn test_lang_string_parse() { t(Default::default()); t(LangString { original: "rust".into(), ..Default::default() }); t(LangString { - original: ".rust".into(), + original: "rusta".into(), rust: false, - unknown: vec![".rust".into()], + unknown: vec!["rusta".into()], ..Default::default() }); - t(LangString { original: "{rust}".into(), rust: false, ..Default::default() }); + // error + t(LangString { original: "{rust}".into(), rust: true, ..Default::default() }); t(LangString { original: "{.rust}".into(), rust: false, @@ -107,9 +108,9 @@ fn test_lang_string_parse() { ..Default::default() }); t(LangString { - original: "test_harness,.rust".into(), + original: "test_harness,rusta".into(), test_harness: true, - unknown: vec![".rust".into()], + unknown: vec!["rusta".into()], ..Default::default() }); t(LangString { @@ -194,65 +195,51 @@ fn test_lang_string_parse() { unknown: vec!["unknown".into()], ..Default::default() }); - t(LangString { - original: "{.first.second}".into(), - added_classes: vec!["first.second".into()], - rust: false, - ..Default::default() - }); - t(LangString { - original: "{class=first=second}".into(), - added_classes: vec!["first=second".into()], - rust: false, - ..Default::default() - }); - t(LangString { - original: "{class=first.second}".into(), - added_classes: vec!["first.second".into()], - rust: false, - ..Default::default() - }); - t(LangString { - original: "{class=.first}".into(), - added_classes: vec![".first".into()], - rust: false, - ..Default::default() - }); + // error + t(LangString { original: "{.first.second}".into(), rust: true, ..Default::default() }); + // error + t(LangString { original: "{class=first=second}".into(), rust: true, ..Default::default() }); + // error + t(LangString { original: "{class=first.second}".into(), rust: true, ..Default::default() }); + // error + t(LangString { original: "{class=.first}".into(), rust: true, ..Default::default() }); t(LangString { original: r#"{class="first"}"#.into(), added_classes: vec!["first".into()], rust: false, ..Default::default() }); - t(LangString { - original: r#"{class=f"irst"}"#.into(), - added_classes: vec!["first".into()], - rust: false, - ..Default::default() - }); + // error + t(LangString { original: r#"{class=f"irst"}"#.into(), rust: true, ..Default::default() }); } #[test] fn test_lang_string_tokenizer() { - fn case(lang_string: &str, want: &[TokenKind<'_>]) { + fn case(lang_string: &str, want: &[LangStringToken<'_>]) { let have = TagIterator::new(lang_string, None).collect::>(); assert_eq!(have, want, "Unexpected lang string split for `{}`", lang_string); } case("", &[]); - case("foo", &[TokenKind::Token("foo")]); - case("foo,bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); - case(".foo,.bar", &[TokenKind::Token(".foo"), TokenKind::Token(".bar")]); - case("{.foo,.bar}", &[TokenKind::Attribute(".foo"), TokenKind::Attribute(".bar")]); - case(" {.foo,.bar} ", &[TokenKind::Attribute(".foo"), TokenKind::Attribute(".bar")]); - case("foo bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); - case("foo\tbar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); - case("foo\t, bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); - case(" foo , bar ", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); - case(",,foo,,bar,,", &[TokenKind::Token("foo"), TokenKind::Token("bar")]); - case("foo=bar", &[TokenKind::Token("foo=bar")]); - case("a-b-c", &[TokenKind::Token("a-b-c")]); - case("a_b_c", &[TokenKind::Token("a_b_c")]); + case("foo", &[LangStringToken::LangToken("foo")]); + case("foo,bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]); + case(".foo,.bar", &[]); + case( + "{.foo,.bar}", + &[LangStringToken::ClassAttribute("foo"), LangStringToken::ClassAttribute("bar")], + ); + case( + " {.foo,.bar} ", + &[LangStringToken::ClassAttribute("foo"), LangStringToken::ClassAttribute("bar")], + ); + case("foo bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]); + case("foo\tbar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]); + case("foo\t, bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]); + case(" foo , bar ", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]); + case(",,foo,,bar,,", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]); + case("foo=bar", &[]); + case("a-b-c", &[LangStringToken::LangToken("a-b-c")]); + case("a_b_c", &[LangStringToken::LangToken("a_b_c")]); } #[test] diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs index c28921b01f133..dd8759b7e378a 100644 --- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs @@ -6,14 +6,80 @@ #![feature(no_core)] #![no_core] -/// ```{. class= whatever=hehe #id} } {{ +/// ```{. } /// main; /// ``` -//~^^^ ERROR missing class name after `.` -//~| ERROR missing class name after `class=` -//~| ERROR unsupported attribute `whatever=hehe` -//~| ERROR unsupported attribute `#id` -//~| ERROR unexpected `}` outside attribute block (`{}`) -//~| ERROR unclosed attribute block (`{}`): missing `}` at the end -//~| ERROR unexpected `{` inside attribute block (`{}`) +//~^^^ ERROR unexpected ` ` character after `.` pub fn foo() {} + +/// ```{class= a} +/// main; +/// ``` +//~^^^ ERROR unexpected ` ` character after `=` +pub fn foo2() {} + +/// ```{#id} +/// main; +/// ``` +//~^^^ ERROR unexpected character `#` +pub fn foo3() {} + +/// ```{{ +/// main; +/// ``` +//~^^^ ERROR unexpected character `{` +pub fn foo4() {} + +/// ```} +/// main; +/// ``` +//~^^^ ERROR unexpected character `}` +pub fn foo5() {} + +/// ```) +/// main; +/// ``` +//~^^^ ERROR unexpected character `)` +pub fn foo6() {} + +/// ```{class=} +/// main; +/// ``` +//~^^^ ERROR unexpected `}` character after `=` +pub fn foo7() {} + +/// ```( +/// main; +/// ``` +//~^^^ ERROR unclosed comment: missing `)` at the end +pub fn foo8() {} + +/// ```{class=one=two} +/// main; +/// ``` +//~^^^ ERROR unexpected `=` +pub fn foo9() {} + +/// ```{.one.two} +/// main; +/// ``` +//~^^^ ERROR unexpected `.` character +pub fn foo10() {} + +/// ```{class=.one} +/// main; +/// ``` +//~^^^ ERROR unexpected `.` character after `=` +pub fn foo11() {} + +/// ```{class=one.two} +/// main; +/// ``` +//~^^^ ERROR unexpected `.` character +pub fn foo12() {} + +/// ```{(comment)} +/// main; +/// ``` +//~^^^ ERROR unexpected character `(` +pub fn foo13() {} diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr index f19b62914dbfb..3e0dc4b2f7a6b 100644 --- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr @@ -1,7 +1,7 @@ -error: missing class name after `.` +error: unexpected ` ` character after `.` --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 | -LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | / /// ```{. } LL | | /// main; LL | | /// ``` | |_______^ @@ -13,53 +13,101 @@ LL | #![deny(warnings)] | ^^^^^^^^ = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]` -error: missing class name after `class=` - --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 +error: unexpected ` ` character after `=` + --> $DIR/custom_code_classes_in_docs-warning.rs:15:1 | -LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | / /// ```{class= a} LL | | /// main; LL | | /// ``` | |_______^ -error: unsupported attribute `whatever=hehe` - --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 +error: unexpected character `#` + --> $DIR/custom_code_classes_in_docs-warning.rs:21:1 | -LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | / /// ```{#id} LL | | /// main; LL | | /// ``` | |_______^ -error: unsupported attribute `#id` - --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 +error: unexpected character `{` + --> $DIR/custom_code_classes_in_docs-warning.rs:27:1 | -LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | / /// ```{{ LL | | /// main; LL | | /// ``` | |_______^ -error: unexpected `}` outside attribute block (`{}`) - --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 +error: unexpected character `}` + --> $DIR/custom_code_classes_in_docs-warning.rs:33:1 | -LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | / /// ```} LL | | /// main; LL | | /// ``` | |_______^ -error: unexpected `{` inside attribute block (`{}`) - --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 +error: unexpected character `)` + --> $DIR/custom_code_classes_in_docs-warning.rs:39:1 | -LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | / /// ```) LL | | /// main; LL | | /// ``` | |_______^ -error: unclosed attribute block (`{}`): missing `}` at the end - --> $DIR/custom_code_classes_in_docs-warning.rs:9:1 +error: unexpected `}` character after `=` + --> $DIR/custom_code_classes_in_docs-warning.rs:45:1 + | +LL | / /// ```{class=} +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unclosed comment: missing `)` at the end + --> $DIR/custom_code_classes_in_docs-warning.rs:51:1 + | +LL | / /// ```( +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unexpected `=` character + --> $DIR/custom_code_classes_in_docs-warning.rs:57:1 + | +LL | / /// ```{class=one=two} +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unexpected `.` character + --> $DIR/custom_code_classes_in_docs-warning.rs:63:1 + | +LL | / /// ```{.one.two} +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unexpected `.` character after `=` + --> $DIR/custom_code_classes_in_docs-warning.rs:69:1 + | +LL | / /// ```{class=.one} +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unexpected `.` character + --> $DIR/custom_code_classes_in_docs-warning.rs:75:1 + | +LL | / /// ```{class=one.two} +LL | | /// main; +LL | | /// ``` + | |_______^ + +error: unexpected character `(` + --> $DIR/custom_code_classes_in_docs-warning.rs:81:1 | -LL | / /// ```{. class= whatever=hehe #id} } {{ +LL | / /// ```{(comment)} LL | | /// main; LL | | /// ``` | |_______^ -error: aborting due to 7 previous errors +error: aborting due to 13 previous errors diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs deleted file mode 100644 index b2ce7407ec6d8..0000000000000 --- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs +++ /dev/null @@ -1,13 +0,0 @@ -// This test ensures that warnings are working as expected for "custom_code_classes_in_docs" -// feature. - -#![feature(custom_code_classes_in_docs)] -#![deny(warnings)] -#![feature(no_core)] -#![no_core] - -/// ```{class=} -/// main; -/// ``` -//~^^^ ERROR missing class name after `class=` -pub fn foo() {} diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr deleted file mode 100644 index 52bb1dae9f61f..0000000000000 --- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr +++ /dev/null @@ -1,17 +0,0 @@ -error: missing class name after `class=` - --> $DIR/custom_code_classes_in_docs-warning2.rs:9:1 - | -LL | / /// ```{class=} -LL | | /// main; -LL | | /// ``` - | |_______^ - | -note: the lint level is defined here - --> $DIR/custom_code_classes_in_docs-warning2.rs:5:9 - | -LL | #![deny(warnings)] - | ^^^^^^^^ - = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]` - -error: aborting due to previous error - diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr index 7432af19360bc..4f2ded78c29fa 100644 --- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr +++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr @@ -1,4 +1,4 @@ -error: unclosed quote string: missing `"` at the end +error: unclosed quote string `"` --> $DIR/custom_code_classes_in_docs-warning3.rs:9:1 | LL | / /// ```{class="} @@ -17,7 +17,7 @@ LL | #![deny(warnings)] | ^^^^^^^^ = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]` -error: unclosed quote string: missing `"` at the end +error: unclosed quote string `"` --> $DIR/custom_code_classes_in_docs-warning3.rs:9:1 | LL | / /// ```{class="} diff --git a/tests/rustdoc/custom_code_classes.rs b/tests/rustdoc/custom_code_classes.rs index f110721c5a715..cd20d8b7d6c9e 100644 --- a/tests/rustdoc/custom_code_classes.rs +++ b/tests/rustdoc/custom_code_classes.rs @@ -22,7 +22,7 @@ /// /// Testing with multiple "unknown". Only the first should be used. /// -/// ```whatever4{.huhu-c} whatever5 +/// ```whatever4,{.huhu-c} whatever5 /// main; /// ``` pub struct Bar; From 5f3002ebeb4afd1dff8bc2dff24c658ac19ee1fd Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 2 May 2023 22:04:15 +0200 Subject: [PATCH 06/13] Add eBNF and documentation on TagIterator --- src/librustdoc/html/markdown.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 0a741f7815b89..482e7b7f260f2 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -855,6 +855,36 @@ pub(crate) enum Ignore { Some(Vec), } +/// This is the parser for fenced codeblocks attributes. It implements the following eBNF: +/// +/// ```eBNF +/// lang-string = *(token-list / delimited-attribute-list / comment) +/// +/// bareword = CHAR *(CHAR) +/// quoted-string = QUOTE *(NONQUOTE) QUOTE +/// token = bareword / quoted-string +/// sep = COMMA/WS *(COMMA/WS) +/// attribute = (DOT token)/(token EQUAL token) +/// attribute-list = [sep] attribute *(sep attribute) [sep] +/// delimited-attribute-list = OPEN-CURLY-BRACKET attribute-list CLOSE-CURLY-BRACKET +/// token-list = [sep] token *(sep token) [sep] +/// comment = OPEN_PAREN *(all characters) CLOSE_PAREN +/// +/// OPEN_PAREN = "(" +/// CLOSE_PARENT = ")" +/// OPEN-CURLY-BRACKET = "{" +/// CLOSE-CURLY-BRACKET = "}" +/// CHAR = ALPHA / DIGIT / "_" / "-" / ":" +/// QUOTE = %x22 +/// NONQUOTE = %x09 / %x20 / %x21 / %x23-7E ; TAB / SPACE / all printable characters except `"` +/// COMMA = "," +/// DOT = "." +/// EQUAL = "=" +/// +/// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +/// DIGIT = %x30-39 +/// WS = %x09 / " " +/// ``` pub(crate) struct TagIterator<'a, 'tcx> { inner: Peekable>, data: &'a str, From 6e21e4823d4bc8f8004271e0b327c7ae4ac9103f Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 3 May 2023 14:02:36 +0200 Subject: [PATCH 07/13] Fix incorrect codeblock attributes in docs --- compiler/rustc_middle/src/ty/typeck_results.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_middle/src/ty/typeck_results.rs b/compiler/rustc_middle/src/ty/typeck_results.rs index 7ecc7e6014dfe..159cbb72a3b72 100644 --- a/compiler/rustc_middle/src/ty/typeck_results.rs +++ b/compiler/rustc_middle/src/ty/typeck_results.rs @@ -165,7 +165,7 @@ pub struct TypeckResults<'tcx> { /// reading places that are mentioned in a closure (because of _ patterns). However, /// to ensure the places are initialized, we introduce fake reads. /// Consider these two examples: - /// ``` (discriminant matching with only wildcard arm) + /// ```ignore (discriminant matching with only wildcard arm) /// let x: u8; /// let c = || match x { _ => () }; /// ``` @@ -173,7 +173,7 @@ pub struct TypeckResults<'tcx> { /// want to capture it. However, we do still want an error here, because `x` should have /// to be initialized at the point where c is created. Therefore, we add a "fake read" /// instead. - /// ``` (destructured assignments) + /// ```ignore (destructured assignments) /// let c = || { /// let (t1, t2) = t; /// } From bbaa930b35667947fb6791527cb8e3a273c8b087 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 3 May 2023 14:44:45 +0200 Subject: [PATCH 08/13] Fix compilation error "the trait bound `SubdiagnosticMessage: From<&std::string::String>` is not satisfied" --- src/librustdoc/passes/check_custom_code_classes.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/librustdoc/passes/check_custom_code_classes.rs b/src/librustdoc/passes/check_custom_code_classes.rs index 246e7f8f3316b..eb32e79643113 100644 --- a/src/librustdoc/passes/check_custom_code_classes.rs +++ b/src/librustdoc/passes/check_custom_code_classes.rs @@ -67,10 +67,11 @@ pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item) .note( // This will list the wrong items to make them more easily searchable. // To ensure the most correct hits, it adds back the 'class:' that was stripped. - &format!( + format!( "found these custom classes: class={}", tests.custom_classes_found.join(",class=") - ), + ) + .as_str(), ) .emit(); } From 87d2aa5fd34ab96f755fe69aed64083e8a246e09 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 4 May 2023 11:17:56 +0200 Subject: [PATCH 09/13] Improve error emitting code --- src/librustdoc/html/markdown.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 482e7b7f260f2..61dc4357c41b0 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -26,6 +26,7 @@ //! ``` use rustc_data_structures::fx::FxHashMap; +use rustc_errors::{DiagnosticMessage, SubdiagnosticMessage}; use rustc_hir::def_id::DefId; use rustc_middle::ty::TyCtxt; pub(crate) use rustc_resolve::rustdoc::main_body_opts; @@ -808,7 +809,7 @@ impl<'tcx> ExtraInfo<'tcx> { ExtraInfo { def_id, sp, tcx } } - fn error_invalid_codeblock_attr(&self, msg: &str) { + fn error_invalid_codeblock_attr(&self, msg: impl Into) { if let Some(def_id) = self.def_id.as_local() { self.tcx.struct_span_lint_hir( crate::lint::INVALID_CODEBLOCK_ATTRIBUTES, @@ -820,7 +821,11 @@ impl<'tcx> ExtraInfo<'tcx> { } } - fn error_invalid_codeblock_attr_with_help(&self, msg: &str, help: &str) { + fn error_invalid_codeblock_attr_with_help( + &self, + msg: impl Into, + help: impl Into, + ) { if let Some(def_id) = self.def_id.as_local() { self.tcx.struct_span_lint_hir( crate::lint::INVALID_CODEBLOCK_ATTRIBUTES, @@ -1246,7 +1251,7 @@ impl LangString { } { if let Some(extra) = extra { extra.error_invalid_codeblock_attr_with_help( - &format!("unknown attribute `{}`. Did you mean `{}`?", x, flag), + format!("unknown attribute `{x}`. Did you mean `{flag}`?"), help, ); } @@ -1263,9 +1268,8 @@ impl LangString { if key == "class" { data.added_classes.push(value.to_owned()); } else if let Some(extra) = extra { - extra.error_invalid_codeblock_attr(&format!( - "unsupported attribute `{key}`" - )); + extra + .error_invalid_codeblock_attr(format!("unsupported attribute `{key}`")); } } LangStringToken::ClassAttribute(class) => { From 113220b970bcecd4463288c459eeeae9ca315bb1 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 28 Aug 2023 14:32:01 +0200 Subject: [PATCH 10/13] Update to new `emit_error` API --- src/librustdoc/html/markdown.rs | 16 ++++++++-------- .../passes/check_custom_code_classes.rs | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 61dc4357c41b0..db30836b43f1d 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -921,7 +921,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { Self { inner: data.char_indices().peekable(), data, is_in_attribute_block: false, extra } } - fn emit_error(&self, err: &str) { + fn emit_error(&self, err: impl Into) { if let Some(extra) = self.extra { extra.error_invalid_codeblock_attr(err); } @@ -954,7 +954,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { } else { let class = &self.data[start + 1..pos]; if class.is_empty() { - self.emit_error(&format!("unexpected `{c}` character after `.`")); + self.emit_error(format!("unexpected `{c}` character after `.`")); return None; } else if self.check_after_token() { return Some(LangStringToken::ClassAttribute(class)); @@ -995,7 +995,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { if let Some((_, c)) = self.inner.next() { if c != '=' { - self.emit_error(&format!("expected `=`, found `{}`", c)); + self.emit_error(format!("expected `=`, found `{}`", c)); return None; } } else { @@ -1006,7 +1006,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { Some((pos, '"')) => self.parse_string(pos)?, Some((pos, c)) if is_bareword_char(c) => self.parse_token(pos)?, Some((_, c)) => { - self.emit_error(&format!("unexpected `{c}` character after `=`")); + self.emit_error(format!("unexpected `{c}` character after `=`")); return None; } None => { @@ -1033,7 +1033,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { if c == '}' || is_separator(c) || c == '(' { true } else { - self.emit_error(&format!("unexpected `{c}` character")); + self.emit_error(format!("unexpected `{c}` character")); false } } else { @@ -1052,7 +1052,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { } else if c == '"' || is_bareword_char(c) { return self.parse_key_value(c, pos); } else { - self.emit_error(&format!("unexpected character `{c}`")); + self.emit_error(format!("unexpected character `{c}`")); return None; } } @@ -1080,7 +1080,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { } let indices = self.parse_string(pos)?; if let Some((_, c)) = self.inner.peek().copied() && c != '{' && !is_separator(c) && c != '(' { - self.emit_error(&format!("expected ` `, `{{` or `,` after `\"`, found `{c}`")); + self.emit_error(format!("expected ` `, `{{` or `,` after `\"`, found `{c}`")); return None; } return Some(LangStringToken::LangToken(&self.data[indices.start..indices.end])); @@ -1103,7 +1103,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> { } return self.next(); } else { - self.emit_error(&format!("unexpected character `{c}`")); + self.emit_error(format!("unexpected character `{c}`")); return None; } } diff --git a/src/librustdoc/passes/check_custom_code_classes.rs b/src/librustdoc/passes/check_custom_code_classes.rs index eb32e79643113..73e80372e4a41 100644 --- a/src/librustdoc/passes/check_custom_code_classes.rs +++ b/src/librustdoc/passes/check_custom_code_classes.rs @@ -54,7 +54,7 @@ pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item) let mut tests = TestsWithCustomClasses { custom_classes_found: vec![] }; - let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); + let dox = item.attrs.doc_value(); find_codes(&dox, &mut tests, ErrorCodes::No, false, None, true); if !tests.custom_classes_found.is_empty() && !cx.tcx.features().custom_code_classes_in_docs { @@ -70,8 +70,7 @@ pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item) format!( "found these custom classes: class={}", tests.custom_classes_found.join(",class=") - ) - .as_str(), + ), ) .emit(); } From f038f180fd5c6a70a6018c2609862d5b6912d761 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 28 Aug 2023 15:03:40 +0200 Subject: [PATCH 11/13] Add `custom` tag for markdown codeblocks --- src/librustdoc/html/markdown.rs | 8 +++--- src/librustdoc/html/markdown/tests.rs | 36 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index db30836b43f1d..177fb1a9426f9 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -1166,6 +1166,7 @@ impl LangString { let allow_error_code_check = allow_error_code_check.as_bool(); let mut seen_rust_tags = false; let mut seen_other_tags = false; + let mut seen_custom_tag = false; let mut data = LangString::default(); let mut ignores = vec![]; @@ -1195,6 +1196,9 @@ impl LangString { data.rust = true; seen_rust_tags = true; } + LangStringToken::LangToken("custom") => { + seen_custom_tag = true; + } LangStringToken::LangToken("test_harness") => { data.test_harness = true; seen_rust_tags = !seen_other_tags || seen_rust_tags; @@ -1264,7 +1268,6 @@ impl LangString { data.unknown.push(x.to_owned()); } LangStringToken::KeyValueAttribute(key, value) => { - seen_other_tags = true; if key == "class" { data.added_classes.push(value.to_owned()); } else if let Some(extra) = extra { @@ -1273,7 +1276,6 @@ impl LangString { } } LangStringToken::ClassAttribute(class) => { - seen_other_tags = true; data.added_classes.push(class.to_owned()); } } @@ -1284,7 +1286,7 @@ impl LangString { data.ignore = Ignore::Some(ignores); } - data.rust &= !seen_other_tags || seen_rust_tags; + data.rust &= !seen_custom_tag && (!seen_other_tags || seen_rust_tags); data } diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index 35b243d3d292e..7d89cb0c4e618 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -64,6 +64,12 @@ fn test_lang_string_parse() { t(LangString { original: "{rust}".into(), rust: true, ..Default::default() }); t(LangString { original: "{.rust}".into(), + rust: true, + added_classes: vec!["rust".into()], + ..Default::default() + }); + t(LangString { + original: "custom,{.rust}".into(), rust: false, added_classes: vec!["rust".into()], ..Default::default() @@ -154,12 +160,24 @@ fn test_lang_string_parse() { t(LangString { original: "{class=test}".into(), added_classes: vec!["test".into()], + rust: true, + ..Default::default() + }); + t(LangString { + original: "custom,{class=test}".into(), + added_classes: vec!["test".into()], rust: false, ..Default::default() }); t(LangString { original: "{.test}".into(), added_classes: vec!["test".into()], + rust: true, + ..Default::default() + }); + t(LangString { + original: "custom,{.test}".into(), + added_classes: vec!["test".into()], rust: false, ..Default::default() }); @@ -172,12 +190,24 @@ fn test_lang_string_parse() { t(LangString { original: "{class=test:with:colon .test1}".into(), added_classes: vec!["test:with:colon".into(), "test1".into()], + rust: true, + ..Default::default() + }); + t(LangString { + original: "custom,{class=test:with:colon .test1}".into(), + added_classes: vec!["test:with:colon".into(), "test1".into()], rust: false, ..Default::default() }); t(LangString { original: "{class=first,class=second}".into(), added_classes: vec!["first".into(), "second".into()], + rust: true, + ..Default::default() + }); + t(LangString { + original: "custom,{class=first,class=second}".into(), + added_classes: vec!["first".into(), "second".into()], rust: false, ..Default::default() }); @@ -206,6 +236,12 @@ fn test_lang_string_parse() { t(LangString { original: r#"{class="first"}"#.into(), added_classes: vec!["first".into()], + rust: true, + ..Default::default() + }); + t(LangString { + original: r#"custom,{class="first"}"#.into(), + added_classes: vec!["first".into()], rust: false, ..Default::default() }); From 7c72edf19fa72a1c9596a79ee9221342a27289ad Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 28 Aug 2023 15:07:54 +0200 Subject: [PATCH 12/13] Update documentation for `custom_code_classes_in_docs` feature --- src/doc/rustdoc/src/unstable-features.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index b5d8c8194188a..0d3f6338af45d 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -631,7 +631,7 @@ to jump to a type definition. ```rust #![feature(custom_code_classes_in_docs)] -/// ```{class=language-c} +/// ```custom,{class=language-c} /// int main(void) { return 0; } /// ``` pub struct Bar; @@ -641,12 +641,16 @@ The text `int main(void) { return 0; }` is rendered without highlighting in a co with the class `language-c`. This can be used to highlight other languages through JavaScript libraries for example. +Without the `custom` attribute, it would be generated as a Rust code example with an additional +`language-C` CSS class. Therefore, if you specifically don't want it to be a Rust code example, +don't forget to add the `custom` attribute. + To be noted that you can replace `class=` with `.` to achieve the same result: ```rust #![feature(custom_code_classes_in_docs)] -/// ```{.language-c} +/// ```custom,{.language-c} /// int main(void) { return 0; } /// ``` pub struct Bar; From e39c39346b8ff10e57fa93563029b4cc39fb2b2a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 16 Sep 2023 11:37:41 +0200 Subject: [PATCH 13/13] Fix invalid markdown codeblock label --- .../rustc_builtin_macros/src/deriving/generic/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs b/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs index 6597ee3cf1b6c..edc6f9f098ea7 100644 --- a/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs +++ b/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs @@ -88,7 +88,7 @@ //! //! When generating the `expr` for the `A` impl, the `SubstructureFields` is //! -//! ```{.text} +//! ```text //! Struct(vec![FieldInfo { //! span: //! name: Some(), @@ -99,7 +99,7 @@ //! //! For the `B` impl, called with `B(a)` and `B(b)`, //! -//! ```{.text} +//! ```text //! Struct(vec![FieldInfo { //! span: , //! name: None, @@ -113,7 +113,7 @@ //! When generating the `expr` for a call with `self == C0(a)` and `other //! == C0(b)`, the SubstructureFields is //! -//! ```{.text} +//! ```text //! EnumMatching(0, , //! vec![FieldInfo { //! span: @@ -125,7 +125,7 @@ //! //! For `C1 {x}` and `C1 {x}`, //! -//! ```{.text} +//! ```text //! EnumMatching(1, , //! vec![FieldInfo { //! span: @@ -137,7 +137,7 @@ //! //! For the tags, //! -//! ```{.text} +//! ```text //! EnumTag( //! &[, ], ) //! ``` @@ -149,7 +149,7 @@ //! //! A static method on the types above would result in, //! -//! ```{.text} +//! ```text //! StaticStruct(, Named(vec![(, )])) //! //! StaticStruct(, Unnamed(vec![]))