diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index d57cba6420f7b..5fdcc6195a706 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -521,6 +521,103 @@ impl CheckAttrVisitor<'tcx> { } } + fn check_doc_codeblock_attr_value( + &self, + meta: &NestedMetaItem, + doc_codeblock_attr: &str, + is_list: bool, + ) -> bool { + let tcx = self.tcx; + let err_fn = move |span: Span, msg: &str| { + tcx.sess.span_err( + span, + &format!( + "`#[doc(codeblock_attr{})]` {}", + if is_list { "(\"...\")" } else { " = \"...\"" }, + msg, + ), + ); + false + }; + if doc_codeblock_attr.is_empty() { + return err_fn( + meta.name_value_literal_span().unwrap_or_else(|| meta.span()), + "attribute cannot have empty value", + ); + } + if let Some(c) = doc_codeblock_attr + .chars() + .find(|&c| c == ',' || c == '`' || c == '"' || c == '\'' || c.is_whitespace()) + { + self.tcx.sess.span_err( + meta.name_value_literal_span().unwrap_or_else(|| meta.span()), + &format!( + "{:?} character isn't allowed in `#[doc(codeblock_attr{})]`", + c, + if is_list { "(\"...\")" } else { " = \"...\"" }, + ), + ); + return false; + } + if doc_codeblock_attr.starts_with(' ') || doc_codeblock_attr.ends_with(' ') { + return err_fn( + meta.name_value_literal_span().unwrap_or_else(|| meta.span()), + "cannot start or end with ' '", + ); + } + true + } + + fn check_doc_codeblock_attr(&self, meta: &NestedMetaItem) -> bool { + if let Some(values) = meta.meta_item_list() { + let mut errors = 0; + for v in values { + match v.literal() { + Some(l) => match l.kind { + LitKind::Str(s, _) => { + if !self.check_doc_codeblock_attr_value(v, &s.as_str(), true) { + errors += 1; + } + } + _ => { + self.tcx + .sess + .struct_span_err( + v.span(), + "`#[doc(codeblock_attr(\"a\"))]` expects string literals", + ) + .emit(); + errors += 1; + } + }, + None => { + self.tcx + .sess + .struct_span_err( + v.span(), + "`#[doc(codeblock_attr(\"a\"))]` expects string literals", + ) + .emit(); + errors += 1; + } + } + } + errors == 0 + } else if let Some(doc_codeblock_attr) = meta.value_str().map(|s| s.to_string()) { + self.check_doc_codeblock_attr_value(meta, &doc_codeblock_attr, false) + } else { + self.tcx + .sess + .struct_span_err( + meta.span(), + "doc codeblock_attr attribute expects a string `#[doc(codeblock_attr = \"a\")]` or a list of \ + strings `#[doc(codeblock_attr(\"a\", \"b\"))]`", + ) + .emit(); + false + } + } + fn check_doc_keyword(&self, meta: &NestedMetaItem, hir_id: HirId) -> bool { let doc_keyword = meta.value_str().map(|s| s.to_string()).unwrap_or_else(String::new); if doc_keyword.is_empty() { @@ -600,6 +697,10 @@ impl CheckAttrVisitor<'tcx> { is_valid = false } + sym::codeblock_attr if !self.check_doc_codeblock_attr(&meta) => { + is_valid = false + } + sym::keyword if !self.check_attr_crate_level(&meta, hir_id, "keyword") || !self.check_doc_keyword(&meta, hir_id) => @@ -627,6 +728,7 @@ impl CheckAttrVisitor<'tcx> { // passes: deprecated // plugins: removed, but rustdoc warns about it itself sym::alias + | sym::codeblock_attr | sym::cfg | sym::hidden | sym::html_favicon_url diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 4be187c5208cd..b3f135cf284ee 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -364,6 +364,7 @@ symbols! { cmp, cmpxchg16b_target_feature, cmse_nonsecure_entry, + codeblock_attr, coerce_unsized, cold, column, diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 607a634c1bced..33a0d42ec9507 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -1073,6 +1073,27 @@ impl Attributes { } aliases.into_iter().collect::>().into() } + + crate fn get_codeblock_attrs(&self) -> Box<[String]> { + let mut codeblocks = FxHashSet::default(); + + for attr in self.other_attrs.lists(sym::doc).filter(|a| a.has_name(sym::codeblock_attr)) { + if let Some(values) = attr.meta_item_list() { + for l in values { + match l.literal().unwrap().kind { + ast::LitKind::Str(s, _) => { + codeblocks.insert(s.as_str().to_string()); + } + _ => unreachable!(), + } + } + } else { + codeblocks.insert(attr.value_str().map(|s| s.to_string()).unwrap()); + } + } + + codeblocks.into_iter().collect::>().into() + } } impl PartialEq for Attributes { diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 6f6ed0eb68413..685fdde35f755 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -1115,8 +1115,10 @@ impl<'a, 'hir, 'tcx> HirCollector<'a, 'hir, 'tcx> { .map(|span| span.ctxt().outer_expn().expansion_cause().unwrap_or(span)) .unwrap_or(DUMMY_SP); self.collector.set_position(span); + let syntax_override = attrs.get_codeblock_attrs(); markdown::find_testable_code( &doc, + &syntax_override, self.collector, self.codes, self.collector.enable_per_target_ignores, diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 509f173055775..d3358c7ed68a8 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -625,6 +625,7 @@ impl<'a, I: Iterator>> Iterator for Footnotes<'a, I> { crate fn find_testable_code( doc: &str, + syntax_override: &[String], tests: &mut T, error_codes: ErrorCodes, enable_per_target_ignores: bool, @@ -634,24 +635,24 @@ crate fn find_testable_code( let mut prev_offset = 0; let mut nb_lines = 0; let mut register_header = None; + + let syntax_override = + if syntax_override.is_empty() { None } else { Some(syntax_override.join(",")) }; while let Some((event, offset)) = parser.next() { match event { Event::Start(Tag::CodeBlock(kind)) => { - let block_info = match kind { - CodeBlockKind::Fenced(ref lang) => { - if lang.is_empty() { - Default::default() - } else { - LangString::parse( - lang, - error_codes, - enable_per_target_ignores, - extra_info, - ) - } - } - CodeBlockKind::Indented => Default::default(), + let lang = match &kind { + CodeBlockKind::Fenced(lang) => syntax_override + .as_deref() + .or_else(|| if lang.is_empty() { None } else { Some(&lang) }), + CodeBlockKind::Indented => syntax_override.as_deref(), }; + let block_info = lang + .map(|lang| { + LangString::parse(lang, error_codes, enable_per_target_ignores, extra_info) + }) + .unwrap_or_default(); + if !block_info.rust { continue; } @@ -1248,7 +1249,11 @@ crate struct RustCodeBlock { /// Returns a range of bytes for each code block in the markdown that is tagged as `rust` or /// untagged (and assumed to be rust). -crate fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec { +crate fn rust_code_blocks( + md: &str, + syntax_override: &[String], + extra_info: &ExtraInfo<'_>, +) -> Vec { let mut code_blocks = vec![]; if md.is_empty() { @@ -1257,65 +1262,50 @@ crate fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec { - let syntax = syntax.as_ref(); - let lang_string = if syntax.is_empty() { - Default::default() - } else { - LangString::parse(&*syntax, ErrorCodes::Yes, false, Some(extra_info)) - }; - if !lang_string.rust { - continue; - } - let is_ignore = lang_string.ignore != Ignore::None; - let syntax = if syntax.is_empty() { None } else { Some(syntax.to_owned()) }; - let (code_start, mut code_end) = match p.next() { - Some((Event::Text(_), offset)) => (offset.start, offset.end), - Some((_, sub_offset)) => { - let code = Range { start: sub_offset.start, end: sub_offset.start }; - code_blocks.push(RustCodeBlock { - is_fenced: true, - range: offset, - code, - syntax, - is_ignore, - }); - continue; + let block_syntax = match &syntax { + CodeBlockKind::Fenced(syntax) => syntax_override + .as_deref() + .or_else(|| if syntax.is_empty() { None } else { Some(&syntax) }), + CodeBlockKind::Indented => syntax_override.as_deref(), + }; + + let lang_string = block_syntax + .map(|syntax| LangString::parse(syntax, ErrorCodes::Yes, false, Some(extra_info))) + .unwrap_or_default(); + if !lang_string.rust { + continue; + } + let is_ignore = lang_string.ignore != Ignore::None; + + let (code, range, is_fenced) = match &syntax { + CodeBlockKind::Fenced(_syntax) => { + let code = match p.next() { + Some((Event::Text(_), offset)) => { + let mut code = offset.clone(); + while let Some((Event::Text(_), offset)) = p.next() { + code.end = offset.end; + } + code } - None => { - let code = Range { start: offset.end, end: offset.end }; - code_blocks.push(RustCodeBlock { - is_fenced: true, - range: offset, - code, - syntax, - is_ignore, - }); - continue; + Some((_, sub_offset)) => { + Range { start: sub_offset.start, end: sub_offset.start } } + None => Range { start: offset.end, end: offset.end }, }; - while let Some((Event::Text(_), offset)) = p.next() { - code_end = offset.end; - } - (syntax, code_start, code_end, offset, true, is_ignore) + (code, offset, true) } CodeBlockKind::Indented => { // The ending of the offset goes too far sometime so we reduce it by one in // these cases. if offset.end > offset.start && md.get(offset.end..=offset.end) == Some(&"\n") { - ( - None, - offset.start, - offset.end, - Range { start: offset.start, end: offset.end - 1 }, - false, - false, - ) + (offset.clone(), Range { start: offset.start, end: offset.end - 1 }, false) } else { - (None, offset.start, offset.end, offset, false, false) + (offset.clone(), offset, false) } } }; @@ -1323,8 +1313,8 @@ crate fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec Result<(), String> { collector.set_position(DUMMY_SP); let codes = ErrorCodes::from(options.render_options.unstable_features.is_nightly_build()); - find_testable_code(&input_str, &mut collector, codes, options.enable_per_target_ignores, None); + find_testable_code( + &input_str, + &[], + &mut collector, + codes, + options.enable_per_target_ignores, + None, + ); options.test_args.insert(0, "rustdoctest".to_string()); testing::test_main( diff --git a/src/librustdoc/passes/calculate_doc_coverage.rs b/src/librustdoc/passes/calculate_doc_coverage.rs index c8b82fb1563df..59977d195dd28 100644 --- a/src/librustdoc/passes/calculate_doc_coverage.rs +++ b/src/librustdoc/passes/calculate_doc_coverage.rs @@ -202,9 +202,11 @@ impl<'a, 'b> fold::DocFolder for CoverageCalculator<'a, 'b> { _ => { let has_docs = !i.attrs.doc_strings.is_empty(); let mut tests = Tests { found_tests: 0 }; + let syntax_override = i.attrs.get_codeblock_attrs(); find_testable_code( &i.attrs.collapsed_doc_value().unwrap_or_default(), + &syntax_override, &mut tests, ErrorCodes::No, false, diff --git a/src/librustdoc/passes/check_code_block_syntax.rs b/src/librustdoc/passes/check_code_block_syntax.rs index 68a66806e0476..a201ec6f391e5 100644 --- a/src/librustdoc/passes/check_code_block_syntax.rs +++ b/src/librustdoc/passes/check_code_block_syntax.rs @@ -112,7 +112,8 @@ impl<'a, 'tcx> DocFolder for SyntaxChecker<'a, 'tcx> { if let Some(dox) = &item.attrs.collapsed_doc_value() { let sp = item.attr_span(self.cx.tcx); let extra = crate::html::markdown::ExtraInfo::new_did(self.cx.tcx, item.def_id, sp); - for code_block in markdown::rust_code_blocks(&dox, &extra) { + let syntax_override = item.attrs.get_codeblock_attrs(); + for code_block in markdown::rust_code_blocks(&dox, &syntax_override, &extra) { self.check_rust_syntax(&item, &dox, code_block); } } diff --git a/src/librustdoc/passes/doc_test_lints.rs b/src/librustdoc/passes/doc_test_lints.rs index c8d2263d81d54..0f9bf050f49d7 100644 --- a/src/librustdoc/passes/doc_test_lints.rs +++ b/src/librustdoc/passes/doc_test_lints.rs @@ -91,8 +91,9 @@ crate fn look_for_tests<'tcx>(cx: &DocContext<'tcx>, dox: &str, item: &Item) { }; let mut tests = Tests { found_tests: 0 }; + let syntax_override = item.attrs.get_codeblock_attrs(); - find_testable_code(&dox, &mut tests, ErrorCodes::No, false, None); + find_testable_code(&dox, &syntax_override, &mut tests, ErrorCodes::No, false, None); if tests.found_tests == 0 && cx.tcx.sess.is_nightly_build() { if should_have_doc_example(cx, &item) { diff --git a/src/test/rustdoc-ui/codeblock-badattr.rs b/src/test/rustdoc-ui/codeblock-badattr.rs new file mode 100644 index 0000000000000..a4539748371de --- /dev/null +++ b/src/test/rustdoc-ui/codeblock-badattr.rs @@ -0,0 +1,22 @@ +#![crate_type = "lib"] + +#[doc = "test"] +#[doc(codeblock_attr = "1,2,3")] //~ ERROR +mod a {} + +#[doc = "test"] +#[doc(codeblock_attr = "foo bar")] //~ ERROR +mod b {} + +#[doc = "test"] +#[doc(codeblock_attr(" ", ","))] //~ ERROR +//~^ ERROR +mod c {} + +#[doc = "test"] +#[doc(codeblock_attr("rust"))] // OK! +mod d {} + +#[doc = "test"] +#[doc(codeblock_attr = "rust")] // OK! +mod e {} diff --git a/src/test/rustdoc-ui/codeblock-badattr.stderr b/src/test/rustdoc-ui/codeblock-badattr.stderr new file mode 100644 index 0000000000000..e89337e899e7c --- /dev/null +++ b/src/test/rustdoc-ui/codeblock-badattr.stderr @@ -0,0 +1,26 @@ +error: ',' character isn't allowed in `#[doc(codeblock_attr = "...")]` + --> $DIR/codeblock-badattr.rs:4:24 + | +LL | #[doc(codeblock_attr = "1,2,3")] + | ^^^^^^^ + +error: ' ' character isn't allowed in `#[doc(codeblock_attr = "...")]` + --> $DIR/codeblock-badattr.rs:8:24 + | +LL | #[doc(codeblock_attr = "foo bar")] + | ^^^^^^^^^ + +error: ' ' character isn't allowed in `#[doc(codeblock_attr("..."))]` + --> $DIR/codeblock-badattr.rs:12:22 + | +LL | #[doc(codeblock_attr(" ", ","))] + | ^^^ + +error: ',' character isn't allowed in `#[doc(codeblock_attr("..."))]` + --> $DIR/codeblock-badattr.rs:12:27 + | +LL | #[doc(codeblock_attr(" ", ","))] + | ^^^ + +error: aborting due to 4 previous errors + diff --git a/src/test/rustdoc-ui/codeblock-override.rs b/src/test/rustdoc-ui/codeblock-override.rs new file mode 100644 index 0000000000000..93f9870b14a67 --- /dev/null +++ b/src/test/rustdoc-ui/codeblock-override.rs @@ -0,0 +1,84 @@ +// compile-flags:--test --test-args=--test-threads=1 +// build-pass +// normalize-stdout-test: "src/test/rustdoc-ui" -> "$$DIR" +// normalize-stdout-test "finished in \d+\.\d+s" -> "finished in $$TIME" + +//! Crate documentation +//! +//! This is some perfectly normal code +//! ``` +//! 10 print "hello... +//! ``` +#![doc(codeblock_attr = "text")] + +#![crate_name = "foo"] +#![crate_type = "lib"] + +#[doc = r#" +Test code: +```rust +println!("I am evil") +``` +"#] +#[doc(codeblock_attr = "text")] +fn test1() {} + +#[doc = r#" +Test code: +``` +println!("I am evil") +``` +"#] +#[doc(codeblock_attr = "text")] +fn test2() {} + +#[doc = r#" +Test code: +``` +10 print "hello, I have a dangling quote +``` +"#] +#[doc(codeblock_attr = "text")] +fn test3() {} + +#[doc = r#" +Test code: + + 10 print "hello, I have a dangling quote + +"#] +#[doc(codeblock_attr = "text")] +fn test4() {} + +#[doc = r#" +This is a test: +```text +panic!("Expected") +``` +"#] +#[doc(codeblock_attr = "should_panic")] +#[doc(codeblock_attr = "rust")] +fn panic_test0() {} + +/// This is a test: +/// ```text +/// panic!("Expected") +/// ``` +#[doc(codeblock_attr = "should_panic")] +#[doc(codeblock_attr = "rust")] +fn panic_test1() {} + +/// This is a test: +/// +/// panic!("Expected") +/// +#[doc(codeblock_attr = "should_panic")] +#[doc(codeblock_attr = "rust")] +fn panic_test2() {} + +/// This is a test: +/// +/// panic!("Expected") +/// +#[doc(codeblock_attr ("rust", "should_panic"))] +fn panic_test3() {} diff --git a/src/test/rustdoc-ui/codeblock-override.stdout b/src/test/rustdoc-ui/codeblock-override.stdout new file mode 100644 index 0000000000000..7609063f9f69c --- /dev/null +++ b/src/test/rustdoc-ui/codeblock-override.stdout @@ -0,0 +1,9 @@ + +running 4 tests +test $DIR/codeblock-override.rs - panic_test0 (line 54) ... ok +test $DIR/codeblock-override.rs - panic_test1 (line 64) ... ok +test $DIR/codeblock-override.rs - panic_test2 (line 74) ... ok +test $DIR/codeblock-override.rs - panic_test3 (line 82) ... ok + +test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + diff --git a/src/test/ui/rustdoc/codeblock-override.rs b/src/test/ui/rustdoc/codeblock-override.rs new file mode 100644 index 0000000000000..f1366944aea14 --- /dev/null +++ b/src/test/ui/rustdoc/codeblock-override.rs @@ -0,0 +1,31 @@ +//! Crate documentation +//! +#![doc(codeblock_attr = "text")] + +// build-pass + +#![crate_type = "lib"] + +#[doc = r#" +Test code: +```rust +println!("I am evil") +``` +"#] +#[doc(codeblock_attr = "text")] +fn test1() {} + +/// This is a test: +/// ```text +/// panic!("Expected") +/// ``` +#[doc(codeblock_attr = "should_panic")] +#[doc(codeblock_attr = "rust")] +fn panic_test() {} + +/// This is a test: +/// +/// panic!("Expected") +/// +#[doc(codeblock_attr ("rust", "should_panic"))] +fn panic_test3() {}