Skip to content

Commit 5a93643

Browse files
committed
add lint against unit tests in doctests
1 parent 3664d63 commit 5a93643

File tree

6 files changed

+122
-15
lines changed

6 files changed

+122
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5545,6 +5545,7 @@ Released 2018-09-13
55455545
[`tabs_in_doc_comments`]: https://rust-lang.github.io/rust-clippy/master/index.html#tabs_in_doc_comments
55465546
[`temporary_assignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_assignment
55475547
[`temporary_cstring_as_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_cstring_as_ptr
5548+
[`test_attr_in_doctest`]: https://rust-lang.github.io/rust-clippy/master/index.html#test_attr_in_doctest
55485549
[`tests_outside_test_module`]: https://rust-lang.github.io/rust-clippy/master/index.html#tests_outside_test_module
55495550
[`to_digit_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_digit_is_some
55505551
[`to_string_in_display`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_display

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
140140
crate::doc::MISSING_SAFETY_DOC_INFO,
141141
crate::doc::NEEDLESS_DOCTEST_MAIN_INFO,
142142
crate::doc::SUSPICIOUS_DOC_COMMENTS_INFO,
143+
crate::doc::TEST_ATTR_IN_DOCTEST_INFO,
143144
crate::doc::UNNECESSARY_SAFETY_DOC_INFO,
144145
crate::double_parens::DOUBLE_PARENS_INFO,
145146
crate::drop_forget_ref::DROP_NON_DROP_INFO,

clippy_lints/src/doc/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,36 @@ declare_clippy_lint! {
199199
"presence of `fn main() {` in code examples"
200200
}
201201

202+
declare_clippy_lint! {
203+
/// ### What it does
204+
/// Checks for `#[test]` in doctests
205+
///
206+
/// ### Why is this bad?
207+
/// Code in examples marked as `#[test]` will somewhat
208+
/// surprisingly not be run by `cargo test`.
209+
///
210+
/// ### Examples
211+
/// ```no_run
212+
/// /// An example of a doctest with a `main()` function
213+
/// ///
214+
/// /// # Examples
215+
/// ///
216+
/// /// ```
217+
/// /// #[test]
218+
/// /// fn equality_works() {
219+
/// /// assert_eq!(1_u8, 1);
220+
/// /// }
221+
/// /// ```
222+
/// fn test_attr_in_doctest() {
223+
/// unimplemented!();
224+
/// }
225+
/// ```
226+
#[clippy::version = "1.40.0"]
227+
pub TEST_ATTR_IN_DOCTEST,
228+
suspicious,
229+
"presence of `#[test]` in code examples"
230+
}
231+
202232
declare_clippy_lint! {
203233
/// ### What it does
204234
/// Detects the syntax `['foo']` in documentation comments (notice quotes instead of backticks)
@@ -330,6 +360,7 @@ impl_lint_pass!(DocMarkdown => [
330360
MISSING_ERRORS_DOC,
331361
MISSING_PANICS_DOC,
332362
NEEDLESS_DOCTEST_MAIN,
363+
TEST_ATTR_IN_DOCTEST,
333364
UNNECESSARY_SAFETY_DOC,
334365
SUSPICIOUS_DOC_COMMENTS
335366
]);

clippy_lints/src/doc/needless_doctest_main.rs

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::ops::Range;
22
use std::{io, thread};
33

4-
use crate::doc::NEEDLESS_DOCTEST_MAIN;
4+
use crate::doc::{NEEDLESS_DOCTEST_MAIN, TEST_ATTR_IN_DOCTEST};
55
use clippy_utils::diagnostics::span_lint;
6-
use rustc_ast::{Async, Fn, FnRetTy, ItemKind};
6+
use rustc_ast::{Async, Fn, FnRetTy, Item, ItemKind};
77
use rustc_data_structures::sync::Lrc;
88
use rustc_errors::emitter::EmitterWriter;
99
use rustc_errors::Handler;
@@ -13,14 +13,24 @@ use rustc_parse::parser::ForceCollect;
1313
use rustc_session::parse::ParseSess;
1414
use rustc_span::edition::Edition;
1515
use rustc_span::source_map::{FilePathMapping, SourceMap};
16-
use rustc_span::{sym, FileName};
16+
use rustc_span::{sym, FileName, Pos};
1717

1818
use super::Fragments;
1919

20+
fn get_test_spans(item: &Item, test_attr_spans: &mut Vec<Range<usize>>) {
21+
test_attr_spans.extend(
22+
item.attrs
23+
.iter()
24+
.filter(|attr| attr.has_name(sym::test))
25+
.map(|attr| attr.span.lo().to_usize()..attr.span.hi().to_usize()),
26+
);
27+
}
28+
2029
pub fn check(cx: &LateContext<'_>, text: &str, edition: Edition, range: Range<usize>, fragments: Fragments<'_>) {
21-
fn has_needless_main(code: String, edition: Edition) -> bool {
30+
fn check_code_sample(code: String, edition: Edition) -> (bool, Vec<Range<usize>>) {
2231
rustc_driver::catch_fatal_errors(|| {
2332
rustc_span::create_session_globals_then(edition, || {
33+
let mut test_attr_spans = vec![];
2434
let filename = FileName::anon_source_code(&code);
2535

2636
let fallback_bundle =
@@ -35,17 +45,19 @@ pub fn check(cx: &LateContext<'_>, text: &str, edition: Edition, range: Range<us
3545
Ok(p) => p,
3646
Err(errs) => {
3747
drop(errs);
38-
return false;
48+
return (false, test_attr_spans);
3949
},
4050
};
4151

4252
let mut relevant_main_found = false;
53+
let mut eligible = true;
4354
loop {
4455
match parser.parse_item(ForceCollect::No) {
4556
Ok(Some(item)) => match &item.kind {
4657
ItemKind::Fn(box Fn {
4758
sig, body: Some(block), ..
4859
}) if item.ident.name == sym::main => {
60+
get_test_spans(&item, &mut test_attr_spans);
4961
let is_async = matches!(sig.header.asyncness, Async::Yes { .. });
5062
let returns_nothing = match &sig.decl.output {
5163
FnRetTy::Default(..) => true,
@@ -58,27 +70,32 @@ pub fn check(cx: &LateContext<'_>, text: &str, edition: Edition, range: Range<us
5870
relevant_main_found = true;
5971
} else {
6072
// This main function should not be linted, we're done
61-
return false;
73+
eligible = false;
6274
}
6375
},
76+
// Another function was found; this case is ignored for needless_doctest_main
77+
ItemKind::Fn(box Fn { .. }) => {
78+
eligible = false;
79+
get_test_spans(&item, &mut test_attr_spans);
80+
},
6481
// Tests with one of these items are ignored
6582
ItemKind::Static(..)
6683
| ItemKind::Const(..)
6784
| ItemKind::ExternCrate(..)
68-
| ItemKind::ForeignMod(..)
69-
// Another function was found; this case is ignored
70-
| ItemKind::Fn(..) => return false,
85+
| ItemKind::ForeignMod(..) => {
86+
eligible = false;
87+
},
7188
_ => {},
7289
},
7390
Ok(None) => break,
7491
Err(e) => {
7592
e.cancel();
76-
return false;
93+
return (false, test_attr_spans);
7794
},
7895
}
7996
}
8097

81-
relevant_main_found
98+
(relevant_main_found & eligible, test_attr_spans)
8299
})
83100
})
84101
.ok()
@@ -90,11 +107,16 @@ pub fn check(cx: &LateContext<'_>, text: &str, edition: Edition, range: Range<us
90107
// Because of the global session, we need to create a new session in a different thread with
91108
// the edition we need.
92109
let text = text.to_owned();
93-
if thread::spawn(move || has_needless_main(text, edition))
110+
let (has_main, test_attr_spans) = thread::spawn(move || check_code_sample(text, edition))
94111
.join()
95-
.expect("thread::spawn failed")
96-
&& let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace)
97-
{
112+
.expect("thread::spawn failed");
113+
if has_main && let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace) {
98114
span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
99115
}
116+
for span in test_attr_spans {
117+
let span = (range.start + span.start)..(range.start + span.end);
118+
if let Some(span) = fragments.span(cx, span) {
119+
span_lint(cx, TEST_ATTR_IN_DOCTEST, span, "unit tests in doctest are not executed");
120+
}
121+
}
100122
}

tests/ui/test_attr_in_doctest.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/// This is a test for `#[test]` in doctests
2+
///
3+
/// # Examples
4+
///
5+
/// ```
6+
/// #[test]
7+
/// fn should_be_linted() {
8+
/// assert_eq!(1, 1);
9+
/// }
10+
/// ```
11+
///
12+
/// Make sure we catch multiple tests in one example,
13+
/// and show that we really parse the attr:
14+
///
15+
/// ```
16+
/// #[test]
17+
/// fn should_also_be_linted() {
18+
/// #[cfg(test)]
19+
/// assert!(true);
20+
/// }
21+
///
22+
/// #[test]
23+
/// fn should_be_linted_too() {
24+
/// assert_eq!("#[test]", "
25+
/// #[test]
26+
/// ");
27+
/// }
28+
/// ```
29+
fn test_attr_in_doctests() {}

tests/ui/test_attr_in_doctest.stderr

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
error: unit tests in doctest are not executed
2+
--> $DIR/test_attr_in_doctest.rs:6:5
3+
|
4+
LL | /// #[test]
5+
| ^^^^^^^
6+
|
7+
= note: `-D clippy::test-attr-in-doctest` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::test_attr_in_doctest)]`
9+
10+
error: unit tests in doctest are not executed
11+
--> $DIR/test_attr_in_doctest.rs:16:5
12+
|
13+
LL | /// #[test]
14+
| ^^^^^^^
15+
16+
error: unit tests in doctest are not executed
17+
--> $DIR/test_attr_in_doctest.rs:22:5
18+
|
19+
LL | /// #[test]
20+
| ^^^^^^^
21+
22+
error: aborting due to 3 previous errors
23+

0 commit comments

Comments
 (0)