about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/librustdoc/html/markdown.rs272
-rw-r--r--src/librustdoc/html/markdown/tests.rs89
-rw-r--r--tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs82
-rw-r--r--tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr90
-rw-r--r--tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs13
-rw-r--r--tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr17
-rw-r--r--tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr4
-rw-r--r--tests/rustdoc/custom_code_classes.rs2
8 files changed, 373 insertions, 196 deletions
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 6bd4e775c0e..0a741f7815b 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<usize> {
@@ -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<Indices> {
+        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<LangStringToken<'a>> {
+        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<Indices> {
+        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<LangStringToken<'a>> {
+        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<TokenKind<'a>> {
+    /// 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<LangStringToken<'a>> {
         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<TokenKind<'a>> {
+    /// 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<LangStringToken<'a>> {
         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<Self::Item> {
         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::<Edition>().ok();
                 }
-                TokenKind::Token(x)
+                LangStringToken::LangToken(x)
                     if allow_error_code_check && x.starts_with('E') && x.len() == 5 =>
                 {
                     if x[1..].parse::<u32>().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 b0b4de65cca..35b243d3d29 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::<Vec<_>>();
         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 c28921b01f1..dd8759b7e37 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 f19b62914db..3e0dc4b2f7a 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 b2ce7407ec6..00000000000
--- 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 52bb1dae9f6..00000000000
--- 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 7432af19360..4f2ded78c29 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 f110721c5a7..cd20d8b7d6c 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;