add lint against unit tests in doctests
This commit is contained in:
parent
3664d6328d
commit
0ba9bf9f9a
@ -5545,6 +5545,7 @@ Released 2018-09-13
|
|||||||
[`tabs_in_doc_comments`]: https://rust-lang.github.io/rust-clippy/master/index.html#tabs_in_doc_comments
|
[`tabs_in_doc_comments`]: https://rust-lang.github.io/rust-clippy/master/index.html#tabs_in_doc_comments
|
||||||
[`temporary_assignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_assignment
|
[`temporary_assignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_assignment
|
||||||
[`temporary_cstring_as_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_cstring_as_ptr
|
[`temporary_cstring_as_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_cstring_as_ptr
|
||||||
|
[`test_attr_in_doctest`]: https://rust-lang.github.io/rust-clippy/master/index.html#test_attr_in_doctest
|
||||||
[`tests_outside_test_module`]: https://rust-lang.github.io/rust-clippy/master/index.html#tests_outside_test_module
|
[`tests_outside_test_module`]: https://rust-lang.github.io/rust-clippy/master/index.html#tests_outside_test_module
|
||||||
[`to_digit_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_digit_is_some
|
[`to_digit_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_digit_is_some
|
||||||
[`to_string_in_display`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_display
|
[`to_string_in_display`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_display
|
||||||
|
@ -140,6 +140,7 @@
|
|||||||
crate::doc::MISSING_SAFETY_DOC_INFO,
|
crate::doc::MISSING_SAFETY_DOC_INFO,
|
||||||
crate::doc::NEEDLESS_DOCTEST_MAIN_INFO,
|
crate::doc::NEEDLESS_DOCTEST_MAIN_INFO,
|
||||||
crate::doc::SUSPICIOUS_DOC_COMMENTS_INFO,
|
crate::doc::SUSPICIOUS_DOC_COMMENTS_INFO,
|
||||||
|
crate::doc::TEST_ATTR_IN_DOCTEST_INFO,
|
||||||
crate::doc::UNNECESSARY_SAFETY_DOC_INFO,
|
crate::doc::UNNECESSARY_SAFETY_DOC_INFO,
|
||||||
crate::double_parens::DOUBLE_PARENS_INFO,
|
crate::double_parens::DOUBLE_PARENS_INFO,
|
||||||
crate::drop_forget_ref::DROP_NON_DROP_INFO,
|
crate::drop_forget_ref::DROP_NON_DROP_INFO,
|
||||||
|
@ -199,6 +199,39 @@
|
|||||||
"presence of `fn main() {` in code examples"
|
"presence of `fn main() {` in code examples"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare_clippy_lint! {
|
||||||
|
/// ### What it does
|
||||||
|
/// Checks for `#[test]` in doctests unless they are marked with
|
||||||
|
/// either `ignore`, `no_run` or `compile_fail`.
|
||||||
|
///
|
||||||
|
/// ### Why is this bad?
|
||||||
|
/// Code in examples marked as `#[test]` will somewhat
|
||||||
|
/// surprisingly not be run by `cargo test`. If you really want
|
||||||
|
/// to show how to test stuff in an example, mark it `no_run` to
|
||||||
|
/// make the intent clear.
|
||||||
|
///
|
||||||
|
/// ### Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// /// An example of a doctest with a `main()` function
|
||||||
|
/// ///
|
||||||
|
/// /// # Examples
|
||||||
|
/// ///
|
||||||
|
/// /// ```
|
||||||
|
/// /// #[test]
|
||||||
|
/// /// fn equality_works() {
|
||||||
|
/// /// assert_eq!(1_u8, 1);
|
||||||
|
/// /// }
|
||||||
|
/// /// ```
|
||||||
|
/// fn test_attr_in_doctest() {
|
||||||
|
/// unimplemented!();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[clippy::version = "1.40.0"]
|
||||||
|
pub TEST_ATTR_IN_DOCTEST,
|
||||||
|
suspicious,
|
||||||
|
"presence of `#[test]` in code examples"
|
||||||
|
}
|
||||||
|
|
||||||
declare_clippy_lint! {
|
declare_clippy_lint! {
|
||||||
/// ### What it does
|
/// ### What it does
|
||||||
/// Detects the syntax `['foo']` in documentation comments (notice quotes instead of backticks)
|
/// Detects the syntax `['foo']` in documentation comments (notice quotes instead of backticks)
|
||||||
@ -330,6 +363,7 @@ pub fn new(valid_idents: &[String], check_private_items: bool) -> Self {
|
|||||||
MISSING_ERRORS_DOC,
|
MISSING_ERRORS_DOC,
|
||||||
MISSING_PANICS_DOC,
|
MISSING_PANICS_DOC,
|
||||||
NEEDLESS_DOCTEST_MAIN,
|
NEEDLESS_DOCTEST_MAIN,
|
||||||
|
TEST_ATTR_IN_DOCTEST,
|
||||||
UNNECESSARY_SAFETY_DOC,
|
UNNECESSARY_SAFETY_DOC,
|
||||||
SUSPICIOUS_DOC_COMMENTS
|
SUSPICIOUS_DOC_COMMENTS
|
||||||
]);
|
]);
|
||||||
@ -516,6 +550,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
|
|||||||
let mut in_heading = false;
|
let mut in_heading = false;
|
||||||
let mut is_rust = false;
|
let mut is_rust = false;
|
||||||
let mut no_test = false;
|
let mut no_test = false;
|
||||||
|
let mut ignore = false;
|
||||||
let mut edition = None;
|
let mut edition = None;
|
||||||
let mut ticks_unbalanced = false;
|
let mut ticks_unbalanced = false;
|
||||||
let mut text_to_check: Vec<(CowStr<'_>, Range<usize>)> = Vec::new();
|
let mut text_to_check: Vec<(CowStr<'_>, Range<usize>)> = Vec::new();
|
||||||
@ -531,6 +566,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
|
|||||||
break;
|
break;
|
||||||
} else if item == "no_test" {
|
} else if item == "no_test" {
|
||||||
no_test = true;
|
no_test = true;
|
||||||
|
} else if item == "no_run" || item == "compile_fail" {
|
||||||
|
ignore = true;
|
||||||
}
|
}
|
||||||
if let Some(stripped) = item.strip_prefix("edition") {
|
if let Some(stripped) = item.strip_prefix("edition") {
|
||||||
is_rust = true;
|
is_rust = true;
|
||||||
@ -544,6 +581,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
|
|||||||
End(CodeBlock(_)) => {
|
End(CodeBlock(_)) => {
|
||||||
in_code = false;
|
in_code = false;
|
||||||
is_rust = false;
|
is_rust = false;
|
||||||
|
ignore = false;
|
||||||
},
|
},
|
||||||
Start(Link(_, url, _)) => in_link = Some(url),
|
Start(Link(_, url, _)) => in_link = Some(url),
|
||||||
End(Link(..)) => in_link = None,
|
End(Link(..)) => in_link = None,
|
||||||
@ -597,7 +635,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
|
|||||||
if in_code {
|
if in_code {
|
||||||
if is_rust && !no_test {
|
if is_rust && !no_test {
|
||||||
let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition());
|
let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition());
|
||||||
needless_doctest_main::check(cx, &text, edition, range.clone(), fragments);
|
needless_doctest_main::check(cx, &text, edition, range.clone(), fragments, ignore);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if in_link.is_some() {
|
if in_link.is_some() {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::{io, thread};
|
use std::{io, thread};
|
||||||
|
|
||||||
use crate::doc::NEEDLESS_DOCTEST_MAIN;
|
use crate::doc::{NEEDLESS_DOCTEST_MAIN, TEST_ATTR_IN_DOCTEST};
|
||||||
use clippy_utils::diagnostics::span_lint;
|
use clippy_utils::diagnostics::span_lint;
|
||||||
use rustc_ast::{Async, Fn, FnRetTy, ItemKind};
|
use rustc_ast::{Async, Fn, FnRetTy, Item, ItemKind};
|
||||||
use rustc_data_structures::sync::Lrc;
|
use rustc_data_structures::sync::Lrc;
|
||||||
use rustc_errors::emitter::EmitterWriter;
|
use rustc_errors::emitter::EmitterWriter;
|
||||||
use rustc_errors::Handler;
|
use rustc_errors::Handler;
|
||||||
@ -13,14 +13,33 @@
|
|||||||
use rustc_session::parse::ParseSess;
|
use rustc_session::parse::ParseSess;
|
||||||
use rustc_span::edition::Edition;
|
use rustc_span::edition::Edition;
|
||||||
use rustc_span::source_map::{FilePathMapping, SourceMap};
|
use rustc_span::source_map::{FilePathMapping, SourceMap};
|
||||||
use rustc_span::{sym, FileName};
|
use rustc_span::{sym, FileName, Pos};
|
||||||
|
|
||||||
use super::Fragments;
|
use super::Fragments;
|
||||||
|
|
||||||
pub fn check(cx: &LateContext<'_>, text: &str, edition: Edition, range: Range<usize>, fragments: Fragments<'_>) {
|
fn get_test_spans(item: &Item, test_attr_spans: &mut Vec<Range<usize>>) {
|
||||||
fn has_needless_main(code: String, edition: Edition) -> bool {
|
test_attr_spans.extend(
|
||||||
|
item.attrs
|
||||||
|
.iter()
|
||||||
|
.find(|attr| attr.has_name(sym::test))
|
||||||
|
.map(|attr| attr.span.lo().to_usize()..item.ident.span.hi().to_usize()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(
|
||||||
|
cx: &LateContext<'_>,
|
||||||
|
text: &str,
|
||||||
|
edition: Edition,
|
||||||
|
range: Range<usize>,
|
||||||
|
fragments: Fragments<'_>,
|
||||||
|
ignore: bool,
|
||||||
|
) {
|
||||||
|
// return whether the code contains a needless `fn main` plus a vector of byte position ranges
|
||||||
|
// of all `#[test]` attributes in not ignored code examples
|
||||||
|
fn check_code_sample(code: String, edition: Edition, ignore: bool) -> (bool, Vec<Range<usize>>) {
|
||||||
rustc_driver::catch_fatal_errors(|| {
|
rustc_driver::catch_fatal_errors(|| {
|
||||||
rustc_span::create_session_globals_then(edition, || {
|
rustc_span::create_session_globals_then(edition, || {
|
||||||
|
let mut test_attr_spans = vec![];
|
||||||
let filename = FileName::anon_source_code(&code);
|
let filename = FileName::anon_source_code(&code);
|
||||||
|
|
||||||
let fallback_bundle =
|
let fallback_bundle =
|
||||||
@ -35,17 +54,21 @@ fn has_needless_main(code: String, edition: Edition) -> bool {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(errs) => {
|
Err(errs) => {
|
||||||
drop(errs);
|
drop(errs);
|
||||||
return false;
|
return (false, test_attr_spans);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut relevant_main_found = false;
|
let mut relevant_main_found = false;
|
||||||
|
let mut eligible = true;
|
||||||
loop {
|
loop {
|
||||||
match parser.parse_item(ForceCollect::No) {
|
match parser.parse_item(ForceCollect::No) {
|
||||||
Ok(Some(item)) => match &item.kind {
|
Ok(Some(item)) => match &item.kind {
|
||||||
ItemKind::Fn(box Fn {
|
ItemKind::Fn(box Fn {
|
||||||
sig, body: Some(block), ..
|
sig, body: Some(block), ..
|
||||||
}) if item.ident.name == sym::main => {
|
}) if item.ident.name == sym::main => {
|
||||||
|
if !ignore {
|
||||||
|
get_test_spans(&item, &mut test_attr_spans);
|
||||||
|
}
|
||||||
let is_async = matches!(sig.header.asyncness, Async::Yes { .. });
|
let is_async = matches!(sig.header.asyncness, Async::Yes { .. });
|
||||||
let returns_nothing = match &sig.decl.output {
|
let returns_nothing = match &sig.decl.output {
|
||||||
FnRetTy::Default(..) => true,
|
FnRetTy::Default(..) => true,
|
||||||
@ -58,27 +81,34 @@ fn has_needless_main(code: String, edition: Edition) -> bool {
|
|||||||
relevant_main_found = true;
|
relevant_main_found = true;
|
||||||
} else {
|
} else {
|
||||||
// This main function should not be linted, we're done
|
// This main function should not be linted, we're done
|
||||||
return false;
|
eligible = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Another function was found; this case is ignored for needless_doctest_main
|
||||||
|
ItemKind::Fn(box Fn { .. }) => {
|
||||||
|
eligible = false;
|
||||||
|
if !ignore {
|
||||||
|
get_test_spans(&item, &mut test_attr_spans);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Tests with one of these items are ignored
|
// Tests with one of these items are ignored
|
||||||
ItemKind::Static(..)
|
ItemKind::Static(..)
|
||||||
| ItemKind::Const(..)
|
| ItemKind::Const(..)
|
||||||
| ItemKind::ExternCrate(..)
|
| ItemKind::ExternCrate(..)
|
||||||
| ItemKind::ForeignMod(..)
|
| ItemKind::ForeignMod(..) => {
|
||||||
// Another function was found; this case is ignored
|
eligible = false;
|
||||||
| ItemKind::Fn(..) => return false,
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
},
|
},
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
e.cancel();
|
e.cancel();
|
||||||
return false;
|
return (false, test_attr_spans);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
relevant_main_found
|
(relevant_main_found & eligible, test_attr_spans)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
@ -90,11 +120,16 @@ fn has_needless_main(code: String, edition: Edition) -> bool {
|
|||||||
// Because of the global session, we need to create a new session in a different thread with
|
// Because of the global session, we need to create a new session in a different thread with
|
||||||
// the edition we need.
|
// the edition we need.
|
||||||
let text = text.to_owned();
|
let text = text.to_owned();
|
||||||
if thread::spawn(move || has_needless_main(text, edition))
|
let (has_main, test_attr_spans) = thread::spawn(move || check_code_sample(text, edition, ignore))
|
||||||
.join()
|
.join()
|
||||||
.expect("thread::spawn failed")
|
.expect("thread::spawn failed");
|
||||||
&& let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace)
|
if has_main && let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace) {
|
||||||
{
|
|
||||||
span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
|
span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
|
||||||
}
|
}
|
||||||
|
for span in test_attr_spans {
|
||||||
|
let span = (range.start + span.start)..(range.start + span.end);
|
||||||
|
if let Some(span) = fragments.span(cx, span) {
|
||||||
|
span_lint(cx, TEST_ATTR_IN_DOCTEST, span, "unit tests in doctest are not executed");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
51
tests/ui/test_attr_in_doctest.rs
Normal file
51
tests/ui/test_attr_in_doctest.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/// This is a test for `#[test]` in doctests
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[test]
|
||||||
|
/// fn should_be_linted() {
|
||||||
|
/// assert_eq!(1, 1);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Make sure we catch multiple tests in one example,
|
||||||
|
/// and show that we really parse the attr:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[test]
|
||||||
|
/// fn should_also_be_linted() {
|
||||||
|
/// #[cfg(test)]
|
||||||
|
/// assert!(true);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[test]
|
||||||
|
/// fn should_be_linted_too() {
|
||||||
|
/// assert_eq!("#[test]", "
|
||||||
|
/// #[test]
|
||||||
|
/// ");
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// We don't catch examples that aren't run:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[test]
|
||||||
|
/// fn ignored() { todo!() }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// #[test]
|
||||||
|
/// fn ignored() { todo!() }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// #[test]
|
||||||
|
/// fn ignored() { Err(()) }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```txt
|
||||||
|
/// #[test]
|
||||||
|
/// fn not_even_rust() { panic!("Ouch") }
|
||||||
|
/// ```
|
||||||
|
fn test_attr_in_doctests() {}
|
29
tests/ui/test_attr_in_doctest.stderr
Normal file
29
tests/ui/test_attr_in_doctest.stderr
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
error: unit tests in doctest are not executed
|
||||||
|
--> $DIR/test_attr_in_doctest.rs:6:5
|
||||||
|
|
|
||||||
|
LL | /// #[test]
|
||||||
|
| _____^
|
||||||
|
LL | | /// fn should_be_linted() {
|
||||||
|
| |_______________________^
|
||||||
|
|
|
||||||
|
= note: `-D clippy::test-attr-in-doctest` implied by `-D warnings`
|
||||||
|
= help: to override `-D warnings` add `#[allow(clippy::test_attr_in_doctest)]`
|
||||||
|
|
||||||
|
error: unit tests in doctest are not executed
|
||||||
|
--> $DIR/test_attr_in_doctest.rs:16:5
|
||||||
|
|
|
||||||
|
LL | /// #[test]
|
||||||
|
| _____^
|
||||||
|
LL | | /// fn should_also_be_linted() {
|
||||||
|
| |____________________________^
|
||||||
|
|
||||||
|
error: unit tests in doctest are not executed
|
||||||
|
--> $DIR/test_attr_in_doctest.rs:22:5
|
||||||
|
|
|
||||||
|
LL | /// #[test]
|
||||||
|
| _____^
|
||||||
|
LL | | /// fn should_be_linted_too() {
|
||||||
|
| |___________________________^
|
||||||
|
|
||||||
|
error: aborting due to 3 previous errors
|
||||||
|
|
Loading…
Reference in New Issue
Block a user