Skip to content

Commit 41bafc4

Browse files
committedSep 16, 2023
Auto merge of #110800 - GuillaumeGomez:custom_code_classes_in_docs, r=t-rustdoc
Accept additional user-defined syntax classes in fenced code blocks Part of #79483. This is a re-opening of #79454 after a big update/cleanup. I also converted the syntax to pandoc as suggested by `@notriddle:` the idea is to be as compatible as possible with the existing instead of having our own syntax. ## Motivation From the original issue: #78917 > The technique used by `inline-c-rs` can be ported to other languages. It's just super fun to see C code inside Rust documentation that is also tested by `cargo doc`. I'm sure this technique can be used by other languages in the future. Having custom CSS classes for syntax highlighting will allow tools like `highlight.js` to be used in order to provide highlighting for languages other than Rust while not increasing technical burden on rustdoc. ## What is the feature about? In short, this PR changes two things, both related to codeblocks in doc comments in Rust documentation: * Allow to disable generation of `language-*` CSS classes with the `custom` attribute. * Add your own CSS classes to a code block so that you can use other tools to highlight them. #### The `custom` attribute Let's start with the new `custom` attribute: it will disable the generation of the `language-*` CSS class on the generated HTML code block. For example: ```rust /// ```custom,c /// int main(void) { /// return 0; /// } /// ``` ``` The generated HTML code block will not have `class="language-c"` because the `custom` attribute has been set. The `custom` attribute becomes especially useful with the other thing added by this feature: adding your own CSS classes. #### Adding your own CSS classes The second part of this feature is to allow users to add CSS classes themselves so that they can then add a JS library which will do it (like `highlight.js` or `prism.js`), allowing to support highlighting for other languages than Rust without increasing burden on rustdoc. To disable the automatic `language-*` CSS class generation, you need to use the `custom` attribute as well. This allow users to write the following: ```rust /// Some code block with `{class=language-c}` as the language string. /// /// ```custom,{class=language-c} /// int main(void) { /// return 0; /// } /// ``` fn main() {} ``` This will notably produce the following HTML: ```html <pre class="language-c"> int main(void) { return 0; }</pre> ``` Instead of: ```html <pre class="rust rust-example-rendered"> <span class="ident">int</span> <span class="ident">main</span>(<span class="ident">void</span>) { <span class="kw">return</span> <span class="number">0</span>; } </pre> ``` To be noted, we could have written `{.language-c}` to achieve the same result. `.` and `class=` have the same effect. One last syntax point: content between parens (`(like this)`) is now considered as comment and is not taken into account at all. In addition to this, I added an `unknown` field into `LangString` (the parsed code block "attribute") because of cases like this: ```rust /// ```custom,class:language-c /// main; /// ``` pub fn foo() {} ``` Without this `unknown` field, it would generate in the DOM: `<pre class="language-class:language-c language-c">`, which is quite bad. So instead, it now stores all unknown tags into the `unknown` field and use the first one as "language". So in this case, since there is no unknown tag, it'll simply generate `<pre class="language-c">`. I added tests to cover this. Finally, I added a parser for the codeblock attributes to make it much easier to maintain. It'll be pretty easy to extend. As to why this syntax for adding attributes was picked: it's [Pandoc's syntax](https://pandoc.org/MANUAL.html#extension-fenced_code_attributes). Even if it seems clunkier in some cases, it's extensible, and most third-party Markdown renderers are smart enough to ignore Pandoc's brace-delimited attributes (from [this comment](#110800 (comment))). ## Raised concerns #### It's not obvious when the `language-*` attribute generation will be added or not. It is added by default. If you want to disable it, you will need to use the `custom` attribute. #### Why not using HTML in markdown directly then? Code examples in most languages are likely to contain `<`, `>`, `&` and `"` characters. These characters [require escaping](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre) when written inside the `<pre>` element. Using the \`\`\` code blocks allows rustdoc to take care of escaping, which means doc authors can paste code samples directly without manually converting them to HTML. cc `@poliorcetics` r? `@notriddle`
·
1.88.01.74.0
2 parents 42dead4 + e39c393 commit 41bafc4

18 files changed

+971
-80
lines changed
 

‎compiler/rustc_builtin_macros/src/deriving/generic/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
//!
8989
//! When generating the `expr` for the `A` impl, the `SubstructureFields` is
9090
//!
91-
//! ```{.text}
91+
//! ```text
9292
//! Struct(vec![FieldInfo {
9393
//! span: <span of x>
9494
//! name: Some(<ident of x>),
@@ -99,7 +99,7 @@
9999
//!
100100
//! For the `B` impl, called with `B(a)` and `B(b)`,
101101
//!
102-
//! ```{.text}
102+
//! ```text
103103
//! Struct(vec![FieldInfo {
104104
//! span: <span of `i32`>,
105105
//! name: None,
@@ -113,7 +113,7 @@
113113
//! When generating the `expr` for a call with `self == C0(a)` and `other
114114
//! == C0(b)`, the SubstructureFields is
115115
//!
116-
//! ```{.text}
116+
//! ```text
117117
//! EnumMatching(0, <ast::Variant for C0>,
118118
//! vec![FieldInfo {
119119
//! span: <span of i32>
@@ -125,7 +125,7 @@
125125
//!
126126
//! For `C1 {x}` and `C1 {x}`,
127127
//!
128-
//! ```{.text}
128+
//! ```text
129129
//! EnumMatching(1, <ast::Variant for C1>,
130130
//! vec![FieldInfo {
131131
//! span: <span of x>
@@ -137,7 +137,7 @@
137137
//!
138138
//! For the tags,
139139
//!
140-
//! ```{.text}
140+
//! ```text
141141
//! EnumTag(
142142
//! &[<ident of self tag>, <ident of other tag>], <expr to combine with>)
143143
//! ```
@@ -149,7 +149,7 @@
149149
//!
150150
//! A static method on the types above would result in,
151151
//!
152-
//! ```{.text}
152+
//! ```text
153153
//! StaticStruct(<ast::VariantData of A>, Named(vec![(<ident of x>, <span of x>)]))
154154
//!
155155
//! StaticStruct(<ast::VariantData of B>, Unnamed(vec![<span of x>]))

‎compiler/rustc_feature/src/active.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@ declare_features! (
401401
/// Allows function attribute `#[coverage(on/off)]`, to control coverage
402402
/// instrumentation of that function.
403403
(active, coverage_attribute, "CURRENT_RUSTC_VERSION", Some(84605), None),
404+
/// Allows users to provide classes for fenced code block using `class:classname`.
405+
(active, custom_code_classes_in_docs, "CURRENT_RUSTC_VERSION", Some(79483), None),
404406
/// Allows non-builtin attributes in inner attribute position.
405407
(active, custom_inner_attributes, "1.30.0", Some(54726), None),
406408
/// Allows custom test frameworks with `#![test_runner]` and `#[test_case]`.

‎compiler/rustc_middle/src/ty/typeck_results.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,15 @@ pub struct TypeckResults<'tcx> {
165165
/// reading places that are mentioned in a closure (because of _ patterns). However,
166166
/// to ensure the places are initialized, we introduce fake reads.
167167
/// Consider these two examples:
168-
/// ``` (discriminant matching with only wildcard arm)
168+
/// ```ignore (discriminant matching with only wildcard arm)
169169
/// let x: u8;
170170
/// let c = || match x { _ => () };
171171
/// ```
172172
/// In this example, we don't need to actually read/borrow `x` in `c`, and so we don't
173173
/// want to capture it. However, we do still want an error here, because `x` should have
174174
/// to be initialized at the point where c is created. Therefore, we add a "fake read"
175175
/// instead.
176-
/// ``` (destructured assignments)
176+
/// ```ignore (destructured assignments)
177177
/// let c = || {
178178
/// let (t1, t2) = t;
179179
/// }

‎compiler/rustc_span/src/symbol.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ symbols! {
592592
cttz,
593593
cttz_nonzero,
594594
custom_attribute,
595+
custom_code_classes_in_docs,
595596
custom_derive,
596597
custom_inner_attributes,
597598
custom_mir,

‎src/doc/rustdoc/src/unstable-features.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,3 +625,47 @@ and check the values of `feature`: `foo` and `bar`.
625625

626626
This flag enables the generation of links in the source code pages which allow the reader
627627
to jump to a type definition.
628+
629+
### Custom CSS classes for code blocks
630+
631+
```rust
632+
#![feature(custom_code_classes_in_docs)]
633+
634+
/// ```custom,{class=language-c}
635+
/// int main(void) { return 0; }
636+
/// ```
637+
pub struct Bar;
638+
```
639+
640+
The text `int main(void) { return 0; }` is rendered without highlighting in a code block
641+
with the class `language-c`. This can be used to highlight other languages through JavaScript
642+
libraries for example.
643+
644+
Without the `custom` attribute, it would be generated as a Rust code example with an additional
645+
`language-C` CSS class. Therefore, if you specifically don't want it to be a Rust code example,
646+
don't forget to add the `custom` attribute.
647+
648+
To be noted that you can replace `class=` with `.` to achieve the same result:
649+
650+
```rust
651+
#![feature(custom_code_classes_in_docs)]
652+
653+
/// ```custom,{.language-c}
654+
/// int main(void) { return 0; }
655+
/// ```
656+
pub struct Bar;
657+
```
658+
659+
To be noted, `rust` and `.rust`/`class=rust` have different effects: `rust` indicates that this is
660+
a Rust code block whereas the two others add a "rust" CSS class on the code block.
661+
662+
You can also use double quotes:
663+
664+
```rust
665+
#![feature(custom_code_classes_in_docs)]
666+
667+
/// ```"not rust" {."hello everyone"}
668+
/// int main(void) { return 0; }
669+
/// ```
670+
pub struct Bar;
671+
```

‎src/librustdoc/html/highlight.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ pub(crate) fn render_example_with_highlighting(
5252
out: &mut Buffer,
5353
tooltip: Tooltip,
5454
playground_button: Option<&str>,
55+
extra_classes: &[String],
5556
) {
56-
write_header(out, "rust-example-rendered", None, tooltip);
57+
write_header(out, "rust-example-rendered", None, tooltip, extra_classes);
5758
write_code(out, src, None, None);
5859
write_footer(out, playground_button);
5960
}
@@ -65,7 +66,13 @@ pub(crate) fn render_item_decl_with_highlighting(src: &str, out: &mut Buffer) {
6566
write!(out, "</pre>");
6667
}
6768

68-
fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>, tooltip: Tooltip) {
69+
fn write_header(
70+
out: &mut Buffer,
71+
class: &str,
72+
extra_content: Option<Buffer>,
73+
tooltip: Tooltip,
74+
extra_classes: &[String],
75+
) {
6976
write!(
7077
out,
7178
"<div class=\"example-wrap{}\">",
@@ -100,9 +107,19 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>, to
100107
out.push_buffer(extra);
101108
}
102109
if class.is_empty() {
103-
write!(out, "<pre class=\"rust\">");
110+
write!(
111+
out,
112+
"<pre class=\"rust{}{}\">",
113+
if extra_classes.is_empty() { "" } else { " " },
114+
extra_classes.join(" "),
115+
);
104116
} else {
105-
write!(out, "<pre class=\"rust {class}\">");
117+
write!(
118+
out,
119+
"<pre class=\"rust {class}{}{}\">",
120+
if extra_classes.is_empty() { "" } else { " " },
121+
extra_classes.join(" "),
122+
);
106123
}
107124
write!(out, "<code>");
108125
}

‎src/librustdoc/html/markdown.rs

Lines changed: 356 additions & 45 deletions
Large diffs are not rendered by default.

‎src/librustdoc/html/markdown/tests.rs

Lines changed: 159 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use super::{find_testable_code, plain_text_summary, short_markdown_summary};
2-
use super::{ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownItemInfo};
2+
use super::{
3+
ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, LangStringToken, Markdown,
4+
MarkdownItemInfo, TagIterator,
5+
};
36
use rustc_span::edition::{Edition, DEFAULT_EDITION};
47

58
#[test]
@@ -51,10 +54,32 @@ fn test_lang_string_parse() {
5154

5255
t(Default::default());
5356
t(LangString { original: "rust".into(), ..Default::default() });
54-
t(LangString { original: ".rust".into(), ..Default::default() });
55-
t(LangString { original: "{rust}".into(), ..Default::default() });
56-
t(LangString { original: "{.rust}".into(), ..Default::default() });
57-
t(LangString { original: "sh".into(), rust: false, ..Default::default() });
57+
t(LangString {
58+
original: "rusta".into(),
59+
rust: false,
60+
unknown: vec!["rusta".into()],
61+
..Default::default()
62+
});
63+
// error
64+
t(LangString { original: "{rust}".into(), rust: true, ..Default::default() });
65+
t(LangString {
66+
original: "{.rust}".into(),
67+
rust: true,
68+
added_classes: vec!["rust".into()],
69+
..Default::default()
70+
});
71+
t(LangString {
72+
original: "custom,{.rust}".into(),
73+
rust: false,
74+
added_classes: vec!["rust".into()],
75+
..Default::default()
76+
});
77+
t(LangString {
78+
original: "sh".into(),
79+
rust: false,
80+
unknown: vec!["sh".into()],
81+
..Default::default()
82+
});
5883
t(LangString { original: "ignore".into(), ignore: Ignore::All, ..Default::default() });
5984
t(LangString {
6085
original: "ignore-foo".into(),
@@ -70,41 +95,56 @@ fn test_lang_string_parse() {
7095
compile_fail: true,
7196
..Default::default()
7297
});
73-
t(LangString { original: "no_run,example".into(), no_run: true, ..Default::default() });
98+
t(LangString {
99+
original: "no_run,example".into(),
100+
no_run: true,
101+
unknown: vec!["example".into()],
102+
..Default::default()
103+
});
74104
t(LangString {
75105
original: "sh,should_panic".into(),
76106
should_panic: true,
77107
rust: false,
108+
unknown: vec!["sh".into()],
78109
..Default::default()
79110
});
80-
t(LangString { original: "example,rust".into(), ..Default::default() });
81111
t(LangString {
82-
original: "test_harness,.rust".into(),
112+
original: "example,rust".into(),
113+
unknown: vec!["example".into()],
114+
..Default::default()
115+
});
116+
t(LangString {
117+
original: "test_harness,rusta".into(),
83118
test_harness: true,
119+
unknown: vec!["rusta".into()],
84120
..Default::default()
85121
});
86122
t(LangString {
87123
original: "text, no_run".into(),
88124
no_run: true,
89125
rust: false,
126+
unknown: vec!["text".into()],
90127
..Default::default()
91128
});
92129
t(LangString {
93130
original: "text,no_run".into(),
94131
no_run: true,
95132
rust: false,
133+
unknown: vec!["text".into()],
96134
..Default::default()
97135
});
98136
t(LangString {
99137
original: "text,no_run, ".into(),
100138
no_run: true,
101139
rust: false,
140+
unknown: vec!["text".into()],
102141
..Default::default()
103142
});
104143
t(LangString {
105144
original: "text,no_run,".into(),
106145
no_run: true,
107146
rust: false,
147+
unknown: vec!["text".into()],
108148
..Default::default()
109149
});
110150
t(LangString {
@@ -117,29 +157,125 @@ fn test_lang_string_parse() {
117157
edition: Some(Edition::Edition2018),
118158
..Default::default()
119159
});
160+
t(LangString {
161+
original: "{class=test}".into(),
162+
added_classes: vec!["test".into()],
163+
rust: true,
164+
..Default::default()
165+
});
166+
t(LangString {
167+
original: "custom,{class=test}".into(),
168+
added_classes: vec!["test".into()],
169+
rust: false,
170+
..Default::default()
171+
});
172+
t(LangString {
173+
original: "{.test}".into(),
174+
added_classes: vec!["test".into()],
175+
rust: true,
176+
..Default::default()
177+
});
178+
t(LangString {
179+
original: "custom,{.test}".into(),
180+
added_classes: vec!["test".into()],
181+
rust: false,
182+
..Default::default()
183+
});
184+
t(LangString {
185+
original: "rust,{class=test,.test2}".into(),
186+
added_classes: vec!["test".into(), "test2".into()],
187+
rust: true,
188+
..Default::default()
189+
});
190+
t(LangString {
191+
original: "{class=test:with:colon .test1}".into(),
192+
added_classes: vec!["test:with:colon".into(), "test1".into()],
193+
rust: true,
194+
..Default::default()
195+
});
196+
t(LangString {
197+
original: "custom,{class=test:with:colon .test1}".into(),
198+
added_classes: vec!["test:with:colon".into(), "test1".into()],
199+
rust: false,
200+
..Default::default()
201+
});
202+
t(LangString {
203+
original: "{class=first,class=second}".into(),
204+
added_classes: vec!["first".into(), "second".into()],
205+
rust: true,
206+
..Default::default()
207+
});
208+
t(LangString {
209+
original: "custom,{class=first,class=second}".into(),
210+
added_classes: vec!["first".into(), "second".into()],
211+
rust: false,
212+
..Default::default()
213+
});
214+
t(LangString {
215+
original: "{class=first,.second},unknown".into(),
216+
added_classes: vec!["first".into(), "second".into()],
217+
rust: false,
218+
unknown: vec!["unknown".into()],
219+
..Default::default()
220+
});
221+
t(LangString {
222+
original: "{class=first .second} unknown".into(),
223+
added_classes: vec!["first".into(), "second".into()],
224+
rust: false,
225+
unknown: vec!["unknown".into()],
226+
..Default::default()
227+
});
228+
// error
229+
t(LangString { original: "{.first.second}".into(), rust: true, ..Default::default() });
230+
// error
231+
t(LangString { original: "{class=first=second}".into(), rust: true, ..Default::default() });
232+
// error
233+
t(LangString { original: "{class=first.second}".into(), rust: true, ..Default::default() });
234+
// error
235+
t(LangString { original: "{class=.first}".into(), rust: true, ..Default::default() });
236+
t(LangString {
237+
original: r#"{class="first"}"#.into(),
238+
added_classes: vec!["first".into()],
239+
rust: true,
240+
..Default::default()
241+
});
242+
t(LangString {
243+
original: r#"custom,{class="first"}"#.into(),
244+
added_classes: vec!["first".into()],
245+
rust: false,
246+
..Default::default()
247+
});
248+
// error
249+
t(LangString { original: r#"{class=f"irst"}"#.into(), rust: true, ..Default::default() });
120250
}
121251

122252
#[test]
123253
fn test_lang_string_tokenizer() {
124-
fn case(lang_string: &str, want: &[&str]) {
125-
let have = LangString::tokens(lang_string).collect::<Vec<&str>>();
254+
fn case(lang_string: &str, want: &[LangStringToken<'_>]) {
255+
let have = TagIterator::new(lang_string, None).collect::<Vec<_>>();
126256
assert_eq!(have, want, "Unexpected lang string split for `{}`", lang_string);
127257
}
128258

129259
case("", &[]);
130-
case("foo", &["foo"]);
131-
case("foo,bar", &["foo", "bar"]);
132-
case(".foo,.bar", &["foo", "bar"]);
133-
case("{.foo,.bar}", &["foo", "bar"]);
134-
case(" {.foo,.bar} ", &["foo", "bar"]);
135-
case("foo bar", &["foo", "bar"]);
136-
case("foo\tbar", &["foo", "bar"]);
137-
case("foo\t, bar", &["foo", "bar"]);
138-
case(" foo , bar ", &["foo", "bar"]);
139-
case(",,foo,,bar,,", &["foo", "bar"]);
140-
case("foo=bar", &["foo=bar"]);
141-
case("a-b-c", &["a-b-c"]);
142-
case("a_b_c", &["a_b_c"]);
260+
case("foo", &[LangStringToken::LangToken("foo")]);
261+
case("foo,bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
262+
case(".foo,.bar", &[]);
263+
case(
264+
"{.foo,.bar}",
265+
&[LangStringToken::ClassAttribute("foo"), LangStringToken::ClassAttribute("bar")],
266+
);
267+
case(
268+
" {.foo,.bar} ",
269+
&[LangStringToken::ClassAttribute("foo"), LangStringToken::ClassAttribute("bar")],
270+
);
271+
case("foo bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
272+
case("foo\tbar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
273+
case("foo\t, bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
274+
case(" foo , bar ", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
275+
case(",,foo,,bar,,", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
276+
case("foo=bar", &[]);
277+
case("a-b-c", &[LangStringToken::LangToken("a-b-c")]);
278+
case("a_b_c", &[LangStringToken::LangToken("a_b_c")]);
143279
}
144280

145281
#[test]
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! NIGHTLY & UNSTABLE CHECK: custom_code_classes_in_docs
2+
//!
3+
//! This pass will produce errors when finding custom classes outside of
4+
//! nightly + relevant feature active.
5+
6+
use super::Pass;
7+
use crate::clean::{Crate, Item};
8+
use crate::core::DocContext;
9+
use crate::fold::DocFolder;
10+
use crate::html::markdown::{find_codes, ErrorCodes, LangString};
11+
12+
use rustc_session::parse::feature_err;
13+
use rustc_span::symbol::sym;
14+
15+
pub(crate) const CHECK_CUSTOM_CODE_CLASSES: Pass = Pass {
16+
name: "check-custom-code-classes",
17+
run: check_custom_code_classes,
18+
description: "check for custom code classes without the feature-gate enabled",
19+
};
20+
21+
pub(crate) fn check_custom_code_classes(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
22+
let mut coll = CustomCodeClassLinter { cx };
23+
24+
coll.fold_crate(krate)
25+
}
26+
27+
struct CustomCodeClassLinter<'a, 'tcx> {
28+
cx: &'a DocContext<'tcx>,
29+
}
30+
31+
impl<'a, 'tcx> DocFolder for CustomCodeClassLinter<'a, 'tcx> {
32+
fn fold_item(&mut self, item: Item) -> Option<Item> {
33+
look_for_custom_classes(&self.cx, &item);
34+
Some(self.fold_item_recur(item))
35+
}
36+
}
37+
38+
#[derive(Debug)]
39+
struct TestsWithCustomClasses {
40+
custom_classes_found: Vec<String>,
41+
}
42+
43+
impl crate::doctest::Tester for TestsWithCustomClasses {
44+
fn add_test(&mut self, _: String, config: LangString, _: usize) {
45+
self.custom_classes_found.extend(config.added_classes.into_iter());
46+
}
47+
}
48+
49+
pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item) {
50+
if !item.item_id.is_local() {
51+
// If non-local, no need to check anything.
52+
return;
53+
}
54+
55+
let mut tests = TestsWithCustomClasses { custom_classes_found: vec![] };
56+
57+
let dox = item.attrs.doc_value();
58+
find_codes(&dox, &mut tests, ErrorCodes::No, false, None, true);
59+
60+
if !tests.custom_classes_found.is_empty() && !cx.tcx.features().custom_code_classes_in_docs {
61+
feature_err(
62+
&cx.tcx.sess.parse_sess,
63+
sym::custom_code_classes_in_docs,
64+
item.attr_span(cx.tcx),
65+
"custom classes in code blocks are unstable",
66+
)
67+
.note(
68+
// This will list the wrong items to make them more easily searchable.
69+
// To ensure the most correct hits, it adds back the 'class:' that was stripped.
70+
format!(
71+
"found these custom classes: class={}",
72+
tests.custom_classes_found.join(",class=")
73+
),
74+
)
75+
.emit();
76+
}
77+
}

‎src/librustdoc/passes/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ pub(crate) use self::calculate_doc_coverage::CALCULATE_DOC_COVERAGE;
3535
mod lint;
3636
pub(crate) use self::lint::RUN_LINTS;
3737

38+
mod check_custom_code_classes;
39+
pub(crate) use self::check_custom_code_classes::CHECK_CUSTOM_CODE_CLASSES;
40+
3841
/// A single pass over the cleaned documentation.
3942
///
4043
/// Runs in the compiler context, so it has access to types and traits and the like.
@@ -66,6 +69,7 @@ pub(crate) enum Condition {
6669

6770
/// The full list of passes.
6871
pub(crate) const PASSES: &[Pass] = &[
72+
CHECK_CUSTOM_CODE_CLASSES,
6973
CHECK_DOC_TEST_VISIBILITY,
7074
STRIP_HIDDEN,
7175
STRIP_PRIVATE,
@@ -79,6 +83,7 @@ pub(crate) const PASSES: &[Pass] = &[
7983

8084
/// The list of passes run by default.
8185
pub(crate) const DEFAULT_PASSES: &[ConditionalPass] = &[
86+
ConditionalPass::always(CHECK_CUSTOM_CODE_CLASSES),
8287
ConditionalPass::always(COLLECT_TRAIT_IMPLS),
8388
ConditionalPass::always(CHECK_DOC_TEST_VISIBILITY),
8489
ConditionalPass::new(STRIP_HIDDEN, WhenNotDocumentHidden),
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// This test ensures that warnings are working as expected for "custom_code_classes_in_docs"
2+
// feature.
3+
4+
#![feature(custom_code_classes_in_docs)]
5+
#![deny(warnings)]
6+
#![feature(no_core)]
7+
#![no_core]
8+
9+
/// ```{. }
10+
/// main;
11+
/// ```
12+
//~^^^ ERROR unexpected ` ` character after `.`
13+
pub fn foo() {}
14+
15+
/// ```{class= a}
16+
/// main;
17+
/// ```
18+
//~^^^ ERROR unexpected ` ` character after `=`
19+
pub fn foo2() {}
20+
21+
/// ```{#id}
22+
/// main;
23+
/// ```
24+
//~^^^ ERROR unexpected character `#`
25+
pub fn foo3() {}
26+
27+
/// ```{{
28+
/// main;
29+
/// ```
30+
//~^^^ ERROR unexpected character `{`
31+
pub fn foo4() {}
32+
33+
/// ```}
34+
/// main;
35+
/// ```
36+
//~^^^ ERROR unexpected character `}`
37+
pub fn foo5() {}
38+
39+
/// ```)
40+
/// main;
41+
/// ```
42+
//~^^^ ERROR unexpected character `)`
43+
pub fn foo6() {}
44+
45+
/// ```{class=}
46+
/// main;
47+
/// ```
48+
//~^^^ ERROR unexpected `}` character after `=`
49+
pub fn foo7() {}
50+
51+
/// ```(
52+
/// main;
53+
/// ```
54+
//~^^^ ERROR unclosed comment: missing `)` at the end
55+
pub fn foo8() {}
56+
57+
/// ```{class=one=two}
58+
/// main;
59+
/// ```
60+
//~^^^ ERROR unexpected `=`
61+
pub fn foo9() {}
62+
63+
/// ```{.one.two}
64+
/// main;
65+
/// ```
66+
//~^^^ ERROR unexpected `.` character
67+
pub fn foo10() {}
68+
69+
/// ```{class=.one}
70+
/// main;
71+
/// ```
72+
//~^^^ ERROR unexpected `.` character after `=`
73+
pub fn foo11() {}
74+
75+
/// ```{class=one.two}
76+
/// main;
77+
/// ```
78+
//~^^^ ERROR unexpected `.` character
79+
pub fn foo12() {}
80+
81+
/// ```{(comment)}
82+
/// main;
83+
/// ```
84+
//~^^^ ERROR unexpected character `(`
85+
pub fn foo13() {}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
error: unexpected ` ` character after `.`
2+
--> $DIR/custom_code_classes_in_docs-warning.rs:9:1
3+
|
4+
LL | / /// ```{. }
5+
LL | | /// main;
6+
LL | | /// ```
7+
| |_______^
8+
|
9+
note: the lint level is defined here
10+
--> $DIR/custom_code_classes_in_docs-warning.rs:5:9
11+
|
12+
LL | #![deny(warnings)]
13+
| ^^^^^^^^
14+
= note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
15+
16+
error: unexpected ` ` character after `=`
17+
--> $DIR/custom_code_classes_in_docs-warning.rs:15:1
18+
|
19+
LL | / /// ```{class= a}
20+
LL | | /// main;
21+
LL | | /// ```
22+
| |_______^
23+
24+
error: unexpected character `#`
25+
--> $DIR/custom_code_classes_in_docs-warning.rs:21:1
26+
|
27+
LL | / /// ```{#id}
28+
LL | | /// main;
29+
LL | | /// ```
30+
| |_______^
31+
32+
error: unexpected character `{`
33+
--> $DIR/custom_code_classes_in_docs-warning.rs:27:1
34+
|
35+
LL | / /// ```{{
36+
LL | | /// main;
37+
LL | | /// ```
38+
| |_______^
39+
40+
error: unexpected character `}`
41+
--> $DIR/custom_code_classes_in_docs-warning.rs:33:1
42+
|
43+
LL | / /// ```}
44+
LL | | /// main;
45+
LL | | /// ```
46+
| |_______^
47+
48+
error: unexpected character `)`
49+
--> $DIR/custom_code_classes_in_docs-warning.rs:39:1
50+
|
51+
LL | / /// ```)
52+
LL | | /// main;
53+
LL | | /// ```
54+
| |_______^
55+
56+
error: unexpected `}` character after `=`
57+
--> $DIR/custom_code_classes_in_docs-warning.rs:45:1
58+
|
59+
LL | / /// ```{class=}
60+
LL | | /// main;
61+
LL | | /// ```
62+
| |_______^
63+
64+
error: unclosed comment: missing `)` at the end
65+
--> $DIR/custom_code_classes_in_docs-warning.rs:51:1
66+
|
67+
LL | / /// ```(
68+
LL | | /// main;
69+
LL | | /// ```
70+
| |_______^
71+
72+
error: unexpected `=` character
73+
--> $DIR/custom_code_classes_in_docs-warning.rs:57:1
74+
|
75+
LL | / /// ```{class=one=two}
76+
LL | | /// main;
77+
LL | | /// ```
78+
| |_______^
79+
80+
error: unexpected `.` character
81+
--> $DIR/custom_code_classes_in_docs-warning.rs:63:1
82+
|
83+
LL | / /// ```{.one.two}
84+
LL | | /// main;
85+
LL | | /// ```
86+
| |_______^
87+
88+
error: unexpected `.` character after `=`
89+
--> $DIR/custom_code_classes_in_docs-warning.rs:69:1
90+
|
91+
LL | / /// ```{class=.one}
92+
LL | | /// main;
93+
LL | | /// ```
94+
| |_______^
95+
96+
error: unexpected `.` character
97+
--> $DIR/custom_code_classes_in_docs-warning.rs:75:1
98+
|
99+
LL | / /// ```{class=one.two}
100+
LL | | /// main;
101+
LL | | /// ```
102+
| |_______^
103+
104+
error: unexpected character `(`
105+
--> $DIR/custom_code_classes_in_docs-warning.rs:81:1
106+
|
107+
LL | / /// ```{(comment)}
108+
LL | | /// main;
109+
LL | | /// ```
110+
| |_______^
111+
112+
error: aborting due to 13 previous errors
113+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// This test ensures that warnings are working as expected for "custom_code_classes_in_docs"
2+
// feature.
3+
4+
#![feature(custom_code_classes_in_docs)]
5+
#![deny(warnings)]
6+
#![feature(no_core)]
7+
#![no_core]
8+
9+
/// ```{class="}
10+
/// main;
11+
/// ```
12+
//~^^^ ERROR unclosed quote string
13+
//~| ERROR unclosed quote string
14+
/// ```"
15+
/// main;
16+
/// ```
17+
pub fn foo() {}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
error: unclosed quote string `"`
2+
--> $DIR/custom_code_classes_in_docs-warning3.rs:9:1
3+
|
4+
LL | / /// ```{class="}
5+
LL | | /// main;
6+
LL | | /// ```
7+
LL | |
8+
... |
9+
LL | | /// main;
10+
LL | | /// ```
11+
| |_______^
12+
|
13+
note: the lint level is defined here
14+
--> $DIR/custom_code_classes_in_docs-warning3.rs:5:9
15+
|
16+
LL | #![deny(warnings)]
17+
| ^^^^^^^^
18+
= note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
19+
20+
error: unclosed quote string `"`
21+
--> $DIR/custom_code_classes_in_docs-warning3.rs:9:1
22+
|
23+
LL | / /// ```{class="}
24+
LL | | /// main;
25+
LL | | /// ```
26+
LL | |
27+
... |
28+
LL | | /// main;
29+
LL | | /// ```
30+
| |_______^
31+
32+
error: aborting due to 2 previous errors
33+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// ```{class=language-c}
2+
/// int main(void) { return 0; }
3+
/// ```
4+
//~^^^ ERROR 1:1: 3:8: custom classes in code blocks are unstable [E0658]
5+
pub struct Bar;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
error[E0658]: custom classes in code blocks are unstable
2+
--> $DIR/feature-gate-custom_code_classes_in_docs.rs:1:1
3+
|
4+
LL | / /// ```{class=language-c}
5+
LL | | /// int main(void) { return 0; }
6+
LL | | /// ```
7+
| |_______^
8+
|
9+
= note: see issue #79483 <https://github.com/rust-lang/rust/issues/79483> for more information
10+
= help: add `#![feature(custom_code_classes_in_docs)]` to the crate attributes to enable
11+
= note: found these custom classes: class=language-c
12+
13+
error: aborting due to previous error
14+
15+
For more information about this error, try `rustc --explain E0658`.

‎tests/rustdoc-ui/issues/issue-91713.stdout

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Available passes for running rustdoc:
2+
check-custom-code-classes - check for custom code classes without the feature-gate enabled
23
check_doc_test_visibility - run various visibility-related lints on doctests
34
strip-hidden - strips all `#[doc(hidden)]` items from the output
45
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
1011
run-lints - runs some of rustdoc's lints
1112

1213
Default passes for rustdoc:
14+
check-custom-code-classes
1315
collect-trait-impls
1416
check_doc_test_visibility
1517
strip-hidden (when not --document-hidden-items)

‎tests/rustdoc/custom_code_classes.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Test for `custom_code_classes_in_docs` feature.
2+
3+
#![feature(custom_code_classes_in_docs)]
4+
#![crate_name = "foo"]
5+
#![feature(no_core)]
6+
#![no_core]
7+
8+
// @has 'foo/struct.Bar.html'
9+
// @has - '//*[@id="main-content"]//pre[@class="language-whatever hoho-c"]' 'main;'
10+
// @has - '//*[@id="main-content"]//pre[@class="language-whatever2 haha-c"]' 'main;'
11+
// @has - '//*[@id="main-content"]//pre[@class="language-whatever4 huhu-c"]' 'main;'
12+
13+
/// ```{class=hoho-c},whatever
14+
/// main;
15+
/// ```
16+
///
17+
/// Testing multiple kinds of orders.
18+
///
19+
/// ```whatever2 {class=haha-c}
20+
/// main;
21+
/// ```
22+
///
23+
/// Testing with multiple "unknown". Only the first should be used.
24+
///
25+
/// ```whatever4,{.huhu-c} whatever5
26+
/// main;
27+
/// ```
28+
pub struct Bar;

0 commit comments

Comments
 (0)
Please sign in to comment.