diff --git a/compiler/rustc_feature/src/active.rs b/compiler/rustc_feature/src/active.rs index ee2fce34b0b0d..4fcf9e6ec3fa6 100644 --- a/compiler/rustc_feature/src/active.rs +++ b/compiler/rustc_feature/src/active.rs @@ -126,7 +126,7 @@ declare_features! ( /// macros disappear). (active, allow_internal_unsafe, "1.0.0", None, None), - /// no-tracking-issue-end + // no-tracking-issue-end /// Allows using `#[link_name="llvm.*"]`. (active, link_llvm_intrinsics, "1.0.0", Some(29602), None), @@ -644,6 +644,9 @@ declare_features! ( /// Allows `extern "C-unwind" fn` to enable unwinding across ABI boundaries. (active, c_unwind, "1.52.0", Some(74990), None), + /// Allows users to provide classes for fenced code block using `class:classname`. + (active, custom_code_classes_in_docs, "1.52.0", Some(79483), None), + // ------------------------------------------------------------------------- // feature-group-end: actual feature gates // ------------------------------------------------------------------------- diff --git a/compiler/rustc_mir/src/borrow_check/region_infer/mod.rs b/compiler/rustc_mir/src/borrow_check/region_infer/mod.rs index bbd512fd36050..9dc2e3d292359 100644 --- a/compiler/rustc_mir/src/borrow_check/region_infer/mod.rs +++ b/compiler/rustc_mir/src/borrow_check/region_infer/mod.rs @@ -1241,7 +1241,7 @@ impl<'tcx> RegionInferenceContext<'tcx> { /// it. However, it works pretty well in practice. In particular, /// this is needed to deal with projection outlives bounds like /// - /// ```ignore (internal compiler representation so lifetime syntax is invalid) + /// ```text (internal compiler representation so lifetime syntax is invalid) /// >::Item: '1 /// ``` /// diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 79ca3c194cc82..7c4658a756143 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -434,6 +434,7 @@ symbols! { cttz, cttz_nonzero, custom_attribute, + custom_code_classes_in_docs, custom_derive, custom_inner_attributes, custom_test_frameworks, diff --git a/compiler/rustc_trait_selection/src/opaque_types.rs b/compiler/rustc_trait_selection/src/opaque_types.rs index 25ba489032bf1..8beb8d64690af 100644 --- a/compiler/rustc_trait_selection/src/opaque_types.rs +++ b/compiler/rustc_trait_selection/src/opaque_types.rs @@ -46,6 +46,7 @@ pub struct OpaqueTypeDecl<'tcx> { /// type Foo = impl Baz; /// fn bar() -> Foo { /// // ^^^ This is the span we are looking for! + /// } /// ``` /// /// In cases where the fn returns `(impl Trait, impl Trait)` or diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index 7d1845dc9578e..ae97d9171f771 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -38,6 +38,20 @@ future. Attempting to use these error numbers on stable will result in the code sample being interpreted as plain text. +### Custom CSS classes for code blocks + +```rust +#![feature(custom_code_classes_in_docs)] + +/// ```class:language-c +/// int main(void) { return 0; } +/// ``` +fn main() {} +``` + +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. + ## Extensions to the `#[doc]` attribute These features operate by extending the `#[doc]` attribute, and thus can be caught by the compiler diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index 7e50d72e60f7d..7affd4d4bbaa2 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -27,6 +27,30 @@ crate fn render_with_highlighting( edition: Edition, ) { debug!("highlighting: ================\n{}\n==============", src); + + write_tooltip(out, tooltip); + write_header(out, class); + write_highlighted_code(out, src, edition); + write_footer(out, playground_button); +} + +/// Does not highlight `src` because additional classes have been provided with +/// the `class:` modifier. +crate fn render_with_added_classes( + src: &str, + out: &mut Buffer, + classes: String, + tooltip: Option<(Option, &str)>, +) { + debug!("*not* highlighting: ================\n{}\n==============", src); + + write_tooltip(out, tooltip); + write_header(out, Some(&classes)); + write_raw_code(out, src); + write_footer(out, None); +} + +fn write_tooltip(out: &mut Buffer, tooltip: Option<(Option, &str)>) { if let Some((edition_info, class)) = tooltip { write!( out, @@ -39,17 +63,13 @@ crate fn render_with_highlighting( }, ); } - - write_header(out, class); - write_code(out, &src, edition); - write_footer(out, playground_button); } fn write_header(out: &mut Buffer, class: Option<&str>) { write!(out, "
\n", class.unwrap_or_default());
 }
 
-fn write_code(out: &mut Buffer, src: &str, edition: Edition) {
+fn write_highlighted_code(out: &mut Buffer, src: &str, edition: Edition) {
     // This replace allows to fix how the code source with DOS backline characters is displayed.
     let src = src.replace("\r\n", "\n");
     Classifier::new(&src, edition).highlight(&mut |highlight| {
@@ -65,6 +85,10 @@ fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {
     write!(out, "
{}
\n", playground_button.unwrap_or_default()); } +fn write_raw_code(out: &mut Buffer, src: &str) { + write!(out, "{}", src.replace("\r\n", "\n")); +} + /// How a span of text is classified. Mostly corresponds to token kinds. #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Class { diff --git a/src/librustdoc/html/highlight/tests.rs b/src/librustdoc/html/highlight/tests.rs index 305cf61091dc6..7a241c331e3a7 100644 --- a/src/librustdoc/html/highlight/tests.rs +++ b/src/librustdoc/html/highlight/tests.rs @@ -1,5 +1,7 @@ -use super::write_code; +use super::write_highlighted_code; + use crate::html::format::Buffer; + use expect_test::expect_file; use rustc_span::edition::Edition; @@ -20,7 +22,7 @@ fn test_html_highlighting() { let src = include_str!("fixtures/sample.rs"); let html = { let mut out = Buffer::new(); - write_code(&mut out, src, Edition::Edition2018); + write_highlighted_code(&mut out, src, Edition::Edition2018); format!("{}
{}
\n", STYLE, out.into_inner()) }; expect_file!["fixtures/sample.html"].assert_eq(&html); @@ -31,7 +33,8 @@ fn test_dos_backline() { let src = "pub fn foo() {\r\n\ println!(\"foo\");\r\n\ }\r\n"; + let mut html = Buffer::new(); - write_code(&mut html, src, Edition::Edition2018); + write_highlighted_code(&mut html, src, Edition::Edition2018); expect_file!["fixtures/dos_line.html"].assert_eq(&html.into_inner()); } diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 1505fe0369d86..86ea89e05cfbc 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -207,6 +207,7 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { let should_panic; let ignore; let edition; + let added_classes; if let Some(Event::Start(Tag::CodeBlock(kind))) = event { let parse_result = match kind { CodeBlockKind::Fenced(ref lang) => { @@ -221,6 +222,7 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { should_panic = parse_result.should_panic; ignore = parse_result.ignore; edition = parse_result.edition; + added_classes = parse_result.added_classes; } else { return event; } @@ -289,33 +291,42 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { )) }); - let tooltip = if ignore != Ignore::None { - Some((None, "ignore")) + let (tooltip, special_class) = if ignore != Ignore::None { + (Some((None, "ignore")), " ignore") } else if compile_fail { - Some((None, "compile_fail")) + (Some((None, "compile_fail")), " compile_fail") } else if should_panic { - Some((None, "should_panic")) + (Some((None, "should_panic")), " should_panic") } else if explicit_edition { - Some((Some(edition), "edition")) + (Some((Some(edition), "edition")), " edition") } else { - None + (None, "") }; // insert newline to clearly separate it from the // previous block so we can shorten the html output let mut s = Buffer::new(); s.push_str("\n"); - highlight::render_with_highlighting( - &text, - &mut s, - Some(&format!( - "rust-example-rendered{}", - if let Some((_, class)) = tooltip { format!(" {}", class) } else { String::new() } - )), - playground_button.as_deref(), - tooltip, - edition, - ); + + if added_classes.is_empty() { + highlight::render_with_highlighting( + &text, + &mut s, + Some(&format!("rust-example-rendered{}", special_class)), + playground_button.as_deref(), + tooltip, + edition, + ); + } else { + let classes = if special_class.is_empty() { + added_classes.join(" ") + } else { + format!("{} {}", special_class.trim(), added_classes.join(" ")) + }; + + highlight::render_with_added_classes(&text, &mut s, classes, tooltip); + } + Some(Event::Html(s.into_inner().into())) } } @@ -744,6 +755,7 @@ crate struct LangString { crate error_codes: Vec, crate allow_fail: bool, crate edition: Option, + crate added_classes: Vec, } #[derive(Eq, PartialEq, Clone, Debug)] @@ -766,6 +778,7 @@ impl Default for LangString { error_codes: Vec::new(), allow_fail: false, edition: None, + added_classes: Vec::new(), } } } @@ -775,7 +788,7 @@ impl LangString { 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) } @@ -809,7 +822,7 @@ impl LangString { 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; @@ -868,6 +881,14 @@ impl LangString { seen_other_tags = true; } } + x if x.starts_with("class:") => { + let class = x.trim_start_matches("class:"); + if class.is_empty() { + seen_other_tags = true; + } else { + data.added_classes.push(class.to_owned()); + } + } x if extra.is_some() => { let s = x.to_lowercase(); match if s == "compile-fail" || s == "compile_fail" || s == "compilefail" { diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index ac3ea4c8c5f6f..a32f377b20205 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -118,6 +118,26 @@ fn test_lang_string_parse() { edition: Some(Edition::Edition2018), ..Default::default() }); + t(LangString { + original: "class:test".into(), + added_classes: vec!["test".into()], + ..Default::default() + }); + t(LangString { + original: "rust,class:test".into(), + added_classes: vec!["test".into()], + ..Default::default() + }); + t(LangString { + original: "class:test:with:colon".into(), + added_classes: vec!["test:with:colon".into()], + ..Default::default() + }); + t(LangString { + original: "class:first,class:second".into(), + added_classes: vec!["first".into(), "second".into()], + ..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..c5965eb113671 --- /dev/null +++ b/src/librustdoc/passes/check_custom_code_classes.rs @@ -0,0 +1,83 @@ +//! 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::{span_of_attrs, Pass}; +use crate::clean::{Crate, Item}; +use crate::core::DocContext; +use crate::fold::DocFolder; +use crate::html::markdown::{find_testable_code, ErrorCodes, LangString}; + +use rustc_session::parse::feature_err; +use rustc_span::symbol::sym; + +crate const CHECK_CUSTOM_CODE_CLASSES: Pass = Pass { + name: "check-custom-code-classes", + run: check_custom_code_classes, + description: "check for custom code classes while not in nightly", +}; + +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 { + let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); + + look_for_custom_classes(&self.cx, &dox, &item); + + Some(self.fold_item_recur(item)) + } +} + +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()); + } +} + +crate fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, dox: &str, item: &Item) { + let hir_id = match DocContext::as_local_hir_id(cx.tcx, item.def_id) { + Some(hir_id) => hir_id, + None => { + // If non-local, no need to check anything. + return; + } + }; + + let mut tests = TestsWithCustomClasses { custom_classes_found: vec![] }; + + find_testable_code(&dox, &mut tests, ErrorCodes::No, false, None); + + if !tests.custom_classes_found.is_empty() && !cx.tcx.features().custom_code_classes_in_docs { + debug!("reporting error for {:?} (hid_id={:?})", item, hir_id); + let sp = span_of_attrs(&item.attrs).unwrap_or(item.source.span()); + feature_err( + &cx.tcx.sess.parse_sess, + sym::custom_code_classes_in_docs, + sp, + "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 4c639c8496db3..8f82555194d30 100644 --- a/src/librustdoc/passes/mod.rs +++ b/src/librustdoc/passes/mod.rs @@ -48,6 +48,9 @@ crate use self::calculate_doc_coverage::CALCULATE_DOC_COVERAGE; mod html_tags; crate use self::html_tags::CHECK_INVALID_HTML_TAGS; +mod check_custom_code_classes; +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. @@ -79,6 +82,7 @@ crate enum Condition { /// The full list of passes. crate const PASSES: &[Pass] = &[ + CHECK_CUSTOM_CODE_CLASSES, CHECK_PRIVATE_ITEMS_DOC_TESTS, STRIP_HIDDEN, UNINDENT_COMMENTS, @@ -97,6 +101,7 @@ crate const PASSES: &[Pass] = &[ crate const DEFAULT_PASSES: &[ConditionalPass] = &[ ConditionalPass::always(COLLECT_TRAIT_IMPLS), ConditionalPass::always(UNINDENT_COMMENTS), + ConditionalPass::always(CHECK_CUSTOM_CODE_CLASSES), ConditionalPass::always(CHECK_PRIVATE_ITEMS_DOC_TESTS), ConditionalPass::new(STRIP_HIDDEN, WhenNotDocumentHidden), ConditionalPass::new(STRIP_PRIVATE, WhenNotDocumentPrivate), diff --git a/src/test/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs b/src/test/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs new file mode 100644 index 0000000000000..edde606649a28 --- /dev/null +++ b/src/test/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 fn main() {} diff --git a/src/test/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr b/src/test/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr new file mode 100644 index 0000000000000..e8124e3574618 --- /dev/null +++ b/src/test/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/src/tools/tidy/src/features.rs b/src/tools/tidy/src/features.rs index cb84fd8be6fec..3ea506de31ff2 100644 --- a/src/tools/tidy/src/features.rs +++ b/src/tools/tidy/src/features.rs @@ -88,6 +88,12 @@ pub fn check( &[ &src_path.join("test/ui"), &src_path.join("test/ui-fulldeps"), + // This allows `tidy` to pick up features that are `rustdoc`-specific. + // + // This way, `tidy` is happy because all features have a test file + // and all the features are correctly tested (either through `rustdoc` + // or `rustc`), while avoiding having to run `rustdoc` for all other + // feature files. &src_path.join("test/rustdoc-ui"), ], &mut |path| super::filter_dirs(path),