Skip to content

Allow #![doc(test(attr(..)))] everywhere #140560

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions compiler/rustc_passes/src/check_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1266,13 +1266,17 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
true
}

/// Checks that `doc(test(...))` attribute contains only valid attributes. Returns `true` if
/// valid.
fn check_test_attr(&self, meta: &MetaItemInner, hir_id: HirId) {
/// Checks that `doc(test(...))` attribute contains only valid attributes and are at the right place.
fn check_test_attr(&self, attr: &Attribute, meta: &MetaItemInner, hir_id: HirId) {
if let Some(metas) = meta.meta_item_list() {
for i_meta in metas {
match (i_meta.name(), i_meta.meta_item()) {
(Some(sym::attr | sym::no_crate_inject), _) => {}
(Some(sym::attr), _) => {
// Allowed everywhere like `#[doc]`
}
(Some(sym::no_crate_inject), _) => {
self.check_attr_crate_level(attr, meta, hir_id);
}
(_, Some(m)) => {
self.tcx.emit_node_span_lint(
INVALID_DOC_ATTRIBUTES,
Expand Down Expand Up @@ -1359,9 +1363,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
}

Some(sym::test) => {
if self.check_attr_crate_level(attr, meta, hir_id) {
self.check_test_attr(meta, hir_id);
}
self.check_test_attr(attr, meta, hir_id);
}

Some(
Expand Down
32 changes: 23 additions & 9 deletions src/doc/rustdoc/src/write-documentation/the-doc-attribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,6 @@ But if you include this:

it will not.

### `test(attr(...))`

This form of the `doc` attribute allows you to add arbitrary attributes to all your doctests. For
example, if you want your doctests to fail if they have dead code, you could add this:

```rust,no_run
#![doc(test(attr(deny(dead_code))))]
```

## At the item level

These forms of the `#[doc]` attribute are used on individual items, to control how
Expand Down Expand Up @@ -281,3 +272,26 @@ To get around this limitation, we just add `#[doc(alias = "lib_name_do_something
on the `do_something` method and then it's all good!
Users can now look for `lib_name_do_something` in our crate directly and find
`Obj::do_something`.

### `test(attr(...))`

This form of the `doc` attribute allows you to add arbitrary attributes to all your doctests. For
example, if you want your doctests to fail if they have dead code, you could add this:

```rust,no_run
#![doc(test(attr(deny(dead_code))))]

mod my_mod {
#![doc(test(attr(allow(dead_code))))] // but allow `dead_code` for this module
}
```

`test(attr(..))` attributes are appended to the parent module's, they do not replace the current
list of attributes. In the previous example, both attributes would be present:

```rust,no_run
// For every doctest in `my_mod`

#![deny(dead_code)] // from the crate-root
#![allow(dead_code)] // from `my_mod`
```
49 changes: 29 additions & 20 deletions src/librustdoc/doctest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod runner;
mod rust;

use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{self, Command, Stdio};
Expand All @@ -14,7 +15,7 @@ use std::{panic, str};

pub(crate) use make::DocTestBuilder;
pub(crate) use markdown::test as test_markdown;
use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet};
use rustc_data_structures::fx::{FxHashMap, FxHasher, FxIndexMap, FxIndexSet};
use rustc_errors::emitter::HumanReadableErrorType;
use rustc_errors::{ColorConfig, DiagCtxtHandle};
use rustc_hir as hir;
Expand Down Expand Up @@ -45,8 +46,6 @@ pub(crate) struct GlobalTestOptions {
/// Whether inserting extra indent spaces in code block,
/// default is `false`, only `true` for generating code link of Rust playground
pub(crate) insert_indent_space: bool,
/// Additional crate-level attributes to add to doctests.
pub(crate) attrs: Vec<String>,
/// Path to file containing arguments for the invocation of rustc.
pub(crate) args_file: PathBuf,
}
Expand Down Expand Up @@ -325,7 +324,7 @@ pub(crate) fn run_tests(
rustdoc_options: &Arc<RustdocOptions>,
unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
mut standalone_tests: Vec<test::TestDescAndFn>,
mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,
// We pass this argument so we can drop it manually before using `exit`.
mut temp_dir: Option<TempDir>,
) {
Expand All @@ -340,7 +339,7 @@ pub(crate) fn run_tests(
let mut ran_edition_tests = 0;
let target_str = rustdoc_options.target.to_string();

for (edition, mut doctests) in mergeable_tests {
for (MergeableTestKey { edition, global_crate_attrs_hash }, mut doctests) in mergeable_tests {
if doctests.is_empty() {
continue;
}
Expand All @@ -350,8 +349,8 @@ pub(crate) fn run_tests(

let rustdoc_test_options = IndividualTestOptions::new(
rustdoc_options,
&Some(format!("merged_doctest_{edition}")),
PathBuf::from(format!("doctest_{edition}.rs")),
&Some(format!("merged_doctest_{edition}_{global_crate_attrs_hash}")),
PathBuf::from(format!("doctest_{edition}_{global_crate_attrs_hash}.rs")),
);

for (doctest, scraped_test) in &doctests {
Expand Down Expand Up @@ -413,12 +412,9 @@ fn scrape_test_config(
attrs: &[hir::Attribute],
args_file: PathBuf,
) -> GlobalTestOptions {
use rustc_ast_pretty::pprust;

let mut opts = GlobalTestOptions {
crate_name,
no_crate_inject: false,
attrs: Vec::new(),
insert_indent_space: false,
args_file,
};
Expand All @@ -435,13 +431,7 @@ fn scrape_test_config(
if attr.has_name(sym::no_crate_inject) {
opts.no_crate_inject = true;
}
if attr.has_name(sym::attr)
&& let Some(l) = attr.meta_item_list()
{
for item in l {
opts.attrs.push(pprust::meta_list_item_to_string(item));
}
}
// NOTE: `test(attr(..))` is handled when discovering the individual tests
}

opts
Expand Down Expand Up @@ -889,6 +879,7 @@ pub(crate) struct ScrapedDocTest {
langstr: LangString,
text: String,
name: String,
global_crate_attrs: Vec<String>,
}

impl ScrapedDocTest {
Expand All @@ -898,6 +889,7 @@ impl ScrapedDocTest {
logical_path: Vec<String>,
langstr: LangString,
text: String,
global_crate_attrs: Vec<String>,
) -> Self {
let mut item_path = logical_path.join("::");
item_path.retain(|c| c != ' ');
Expand All @@ -907,7 +899,7 @@ impl ScrapedDocTest {
let name =
format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());

Self { filename, line, langstr, text, name }
Self { filename, line, langstr, text, name, global_crate_attrs }
}
fn edition(&self, opts: &RustdocOptions) -> Edition {
self.langstr.edition.unwrap_or(opts.edition)
Expand Down Expand Up @@ -936,9 +928,15 @@ pub(crate) trait DocTestVisitor {
fn visit_header(&mut self, _name: &str, _level: u32) {}
}

#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub(crate) struct MergeableTestKey {
edition: Edition,
global_crate_attrs_hash: u64,
}

struct CreateRunnableDocTests {
standalone_tests: Vec<test::TestDescAndFn>,
mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,

rustdoc_options: Arc<RustdocOptions>,
opts: GlobalTestOptions,
Expand Down Expand Up @@ -991,6 +989,7 @@ impl CreateRunnableDocTests {
&scraped_test.text,
Some(&self.opts.crate_name),
edition,
scraped_test.global_crate_attrs.clone(),
self.can_merge_doctests,
Some(test_id),
Some(&scraped_test.langstr),
Expand All @@ -1005,7 +1004,17 @@ impl CreateRunnableDocTests {
let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
self.standalone_tests.push(test_desc);
} else {
self.mergeable_tests.entry(edition).or_default().push((doctest, scraped_test));
self.mergeable_tests
.entry(MergeableTestKey {
edition,
global_crate_attrs_hash: {
let mut hasher = FxHasher::default();
scraped_test.global_crate_attrs.hash(&mut hasher);
hasher.finish()
},
})
.or_default()
.push((doctest, scraped_test));
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/librustdoc/doctest/extracted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ impl ExtractedDocTests {
) {
let edition = scraped_test.edition(options);

let ScrapedDocTest { filename, line, langstr, text, name } = scraped_test;
let ScrapedDocTest { filename, line, langstr, text, name, global_crate_attrs } =
scraped_test;

let doctest = DocTestBuilder::new(
&text,
Some(&opts.crate_name),
edition,
global_crate_attrs,
false,
None,
Some(&langstr),
Expand Down
13 changes: 10 additions & 3 deletions src/librustdoc/doctest/make.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(crate) struct DocTestBuilder {
pub(crate) supports_color: bool,
pub(crate) already_has_extern_crate: bool,
pub(crate) has_main_fn: bool,
pub(crate) global_crate_attrs: Vec<String>,
pub(crate) crate_attrs: String,
/// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
/// put into `crate_attrs`.
Expand All @@ -57,6 +58,7 @@ impl DocTestBuilder {
source: &str,
crate_name: Option<&str>,
edition: Edition,
global_crate_attrs: Vec<String>,
can_merge_doctests: bool,
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
test_id: Option<String>,
Expand Down Expand Up @@ -88,6 +90,7 @@ impl DocTestBuilder {
// If the AST returned an error, we don't want this doctest to be merged with the
// others.
return Self::invalid(
Vec::new(),
String::new(),
String::new(),
String::new(),
Expand All @@ -110,6 +113,7 @@ impl DocTestBuilder {
Self {
supports_color,
has_main_fn,
global_crate_attrs,
crate_attrs,
maybe_crate_attrs,
crates,
Expand All @@ -122,6 +126,7 @@ impl DocTestBuilder {
}

fn invalid(
global_crate_attrs: Vec<String>,
crate_attrs: String,
maybe_crate_attrs: String,
crates: String,
Expand All @@ -131,6 +136,7 @@ impl DocTestBuilder {
Self {
supports_color: false,
has_main_fn: false,
global_crate_attrs,
crate_attrs,
maybe_crate_attrs,
crates,
Expand Down Expand Up @@ -160,7 +166,8 @@ impl DocTestBuilder {
let mut line_offset = 0;
let mut prog = String::new();
let everything_else = self.everything_else.trim();
if opts.attrs.is_empty() {

if self.global_crate_attrs.is_empty() {
// If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
// lints that are commonly triggered in doctests. The crate-level test attributes are
// commonly used to make tests fail in case they trigger warnings, so having this there in
Expand All @@ -169,8 +176,8 @@ impl DocTestBuilder {
line_offset += 1;
}

// Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
for attr in &opts.attrs {
// Next, any attributes that came from #![doc(test(attr(...)))].
for attr in &self.global_crate_attrs {
prog.push_str(&format!("#![{attr}]\n"));
line_offset += 1;
}
Expand Down
10 changes: 8 additions & 2 deletions src/librustdoc/doctest/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ impl DocTestVisitor for MdCollector {
let filename = self.filename.clone();
// First line of Markdown is line 1.
let line = 1 + rel_line.offset();
self.tests.push(ScrapedDocTest::new(filename, line, self.cur_path.clone(), config, test));
self.tests.push(ScrapedDocTest::new(
filename,
line,
self.cur_path.clone(),
config,
test,
Vec::new(),
));
}

fn visit_header(&mut self, name: &str, level: u32) {
Expand Down Expand Up @@ -89,7 +96,6 @@ pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
crate_name,
no_crate_inject: true,
insert_indent_space: false,
attrs: vec![],
args_file,
};

Expand Down
11 changes: 8 additions & 3 deletions src/librustdoc/doctest/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::html::markdown::{Ignore, LangString};
/// Convenient type to merge compatible doctests into one.
pub(crate) struct DocTestRunner {
crate_attrs: FxIndexSet<String>,
global_crate_attrs: FxIndexSet<String>,
ids: String,
output: String,
output_merged_tests: String,
Expand All @@ -23,6 +24,7 @@ impl DocTestRunner {
pub(crate) fn new() -> Self {
Self {
crate_attrs: FxIndexSet::default(),
global_crate_attrs: FxIndexSet::default(),
ids: String::new(),
output: String::new(),
output_merged_tests: String::new(),
Expand All @@ -46,6 +48,9 @@ impl DocTestRunner {
for line in doctest.crate_attrs.split('\n') {
self.crate_attrs.insert(line.to_string());
}
for line in &doctest.global_crate_attrs {
self.global_crate_attrs.insert(line.to_string());
}
}
self.ids.push_str(&format!(
"tests.push({}::TEST);\n",
Expand Down Expand Up @@ -85,16 +90,16 @@ impl DocTestRunner {
code_prefix.push('\n');
}

if opts.attrs.is_empty() {
if self.global_crate_attrs.is_empty() {
// If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
// lints that are commonly triggered in doctests. The crate-level test attributes are
// commonly used to make tests fail in case they trigger warnings, so having this there in
// that case may cause some tests to pass when they shouldn't have.
code_prefix.push_str("#![allow(unused)]\n");
}

// Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
for attr in &opts.attrs {
// Next, any attributes that came from #![doc(test(attr(...)))].
for attr in &self.global_crate_attrs {
code_prefix.push_str(&format!("#![{attr}]\n"));
}

Expand Down
Loading
Loading