Skip to content

Commit f8db258

Browse files
committed
Auto merge of #5912 - ebroto:needless_doctest_main_improvements, r=Manishearth,flip1995
Parse doctests in needless_doctest_main This switches from text-based search to running the parser to avoid false positives. Inspired by how [rustdoc](https://github.com/rust-lang/rust/blob/3f3250500fe152b5759c21453ba9a9129808d0d8/src/librustdoc/test.rs#L366) handles this and by #4729. cc @llogiq changelog: Fix multiple false positives in [`needless_doctest_main`]. Fixes #5879 Fixes #4906 Fixes #5103 Fixes #4698
2 parents f0cc006 + 1a140dc commit f8db258

File tree

3 files changed

+142
-13
lines changed

3 files changed

+142
-13
lines changed

clippy_lints/src/doc.rs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
use crate::utils::{implements_trait, is_entrypoint_fn, is_type_diagnostic_item, return_ty, span_lint};
22
use if_chain::if_chain;
33
use itertools::Itertools;
4-
use rustc_ast::ast::{AttrKind, Attribute};
4+
use rustc_ast::ast::{Async, AttrKind, Attribute, FnRetTy, ItemKind};
55
use rustc_ast::token::CommentKind;
66
use rustc_data_structures::fx::FxHashSet;
7+
use rustc_data_structures::sync::Lrc;
8+
use rustc_errors::emitter::EmitterWriter;
9+
use rustc_errors::Handler;
710
use rustc_hir as hir;
811
use rustc_lint::{LateContext, LateLintPass};
912
use rustc_middle::lint::in_external_macro;
1013
use rustc_middle::ty;
14+
use rustc_parse::maybe_new_parser_from_source_str;
15+
use rustc_session::parse::ParseSess;
1116
use rustc_session::{declare_tool_lint, impl_lint_pass};
12-
use rustc_span::source_map::{BytePos, MultiSpan, Span};
13-
use rustc_span::Pos;
17+
use rustc_span::source_map::{BytePos, FilePathMapping, MultiSpan, SourceMap, Span};
18+
use rustc_span::{FileName, Pos};
19+
use std::io;
1420
use std::ops::Range;
1521
use url::Url;
1622

@@ -431,10 +437,67 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
431437
headers
432438
}
433439

434-
static LEAVE_MAIN_PATTERNS: &[&str] = &["static", "fn main() {}", "extern crate", "async fn main() {"];
435-
436440
fn check_code(cx: &LateContext<'_>, text: &str, span: Span) {
437-
if text.contains("fn main() {") && !LEAVE_MAIN_PATTERNS.iter().any(|p| text.contains(p)) {
441+
fn has_needless_main(code: &str) -> bool {
442+
let filename = FileName::anon_source_code(code);
443+
444+
let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
445+
let emitter = EmitterWriter::new(box io::sink(), None, false, false, false, None, false);
446+
let handler = Handler::with_emitter(false, None, box emitter);
447+
let sess = ParseSess::with_span_handler(handler, sm);
448+
449+
let mut parser = match maybe_new_parser_from_source_str(&sess, filename, code.into()) {
450+
Ok(p) => p,
451+
Err(errs) => {
452+
for mut err in errs {
453+
err.cancel();
454+
}
455+
return false;
456+
},
457+
};
458+
459+
let mut relevant_main_found = false;
460+
loop {
461+
match parser.parse_item() {
462+
Ok(Some(item)) => match &item.kind {
463+
// Tests with one of these items are ignored
464+
ItemKind::Static(..)
465+
| ItemKind::Const(..)
466+
| ItemKind::ExternCrate(..)
467+
| ItemKind::ForeignMod(..) => return false,
468+
// We found a main function ...
469+
ItemKind::Fn(_, sig, _, Some(block)) if item.ident.name == sym!(main) => {
470+
let is_async = matches!(sig.header.asyncness, Async::Yes{..});
471+
let returns_nothing = match &sig.decl.output {
472+
FnRetTy::Default(..) => true,
473+
FnRetTy::Ty(ty) if ty.kind.is_unit() => true,
474+
_ => false,
475+
};
476+
477+
if returns_nothing && !is_async && !block.stmts.is_empty() {
478+
// This main function should be linted, but only if there are no other functions
479+
relevant_main_found = true;
480+
} else {
481+
// This main function should not be linted, we're done
482+
return false;
483+
}
484+
},
485+
// Another function was found; this case is ignored too
486+
ItemKind::Fn(..) => return false,
487+
_ => {},
488+
},
489+
Ok(None) => break,
490+
Err(mut e) => {
491+
e.cancel();
492+
return false;
493+
},
494+
}
495+
}
496+
497+
relevant_main_found
498+
}
499+
500+
if has_needless_main(text) {
438501
span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
439502
}
440503
}

tests/ui/needless_doc_main.rs

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@
99
/// }
1010
/// ```
1111
///
12-
/// This should, too.
12+
/// With an explicit return type it should lint too
13+
/// ```
14+
/// fn main() -> () {
15+
/// unimplemented!();
16+
/// }
17+
/// ```
1318
///
19+
/// This should, too.
1420
/// ```rust
1521
/// fn main() {
1622
/// unimplemented!();
1723
/// }
1824
/// ```
1925
///
2026
/// This one too.
21-
///
2227
/// ```no_run
2328
/// fn main() {
2429
/// unimplemented!();
@@ -33,6 +38,20 @@ fn bad_doctests() {}
3338
/// fn main(){}
3439
/// ```
3540
///
41+
/// This shouldn't lint either, because main is async:
42+
/// ```
43+
/// async fn main() {
44+
/// assert_eq!(42, ANSWER);
45+
/// }
46+
/// ```
47+
///
48+
/// Same here, because the return type is not the unit type:
49+
/// ```
50+
/// fn main() -> Result<()> {
51+
/// Ok(())
52+
/// }
53+
/// ```
54+
///
3655
/// This shouldn't lint either, because there's a `static`:
3756
/// ```
3857
/// static ANSWER: i32 = 42;
@@ -42,6 +61,15 @@ fn bad_doctests() {}
4261
/// }
4362
/// ```
4463
///
64+
/// This shouldn't lint either, because there's a `const`:
65+
/// ```
66+
/// fn main() {
67+
/// assert_eq!(42, ANSWER);
68+
/// }
69+
///
70+
/// const ANSWER: i32 = 42;
71+
/// ```
72+
///
4573
/// Neither should this lint because of `extern crate`:
4674
/// ```
4775
/// #![feature(test)]
@@ -51,16 +79,48 @@ fn bad_doctests() {}
5179
/// }
5280
/// ```
5381
///
54-
/// We should not lint ignored examples:
82+
/// Neither should this lint because it has an extern block:
83+
/// ```
84+
/// extern {}
85+
/// fn main() {
86+
/// unimplemented!();
87+
/// }
88+
/// ```
89+
///
90+
/// This should not lint because there is another function defined:
91+
/// ```
92+
/// fn fun() {}
93+
///
94+
/// fn main() {
95+
/// unimplemented!();
96+
/// }
97+
/// ```
5598
///
99+
/// We should not lint inside raw strings ...
100+
/// ```
101+
/// let string = r#"
102+
/// fn main() {
103+
/// unimplemented!();
104+
/// }
105+
/// "#;
106+
/// ```
107+
///
108+
/// ... or comments
109+
/// ```
110+
/// // fn main() {
111+
/// // let _inception = 42;
112+
/// // }
113+
/// let _inception = 42;
114+
/// ```
115+
///
116+
/// We should not lint ignored examples:
56117
/// ```rust,ignore
57118
/// fn main() {
58119
/// unimplemented!();
59120
/// }
60121
/// ```
61122
///
62123
/// Or even non-rust examples:
63-
///
64124
/// ```text
65125
/// fn main() {
66126
/// is what starts the program

tests/ui/needless_doc_main.stderr

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ LL | /// fn main() {
77
= note: `-D clippy::needless-doctest-main` implied by `-D warnings`
88

99
error: needless `fn main` in doctest
10-
--> $DIR/needless_doc_main.rs:15:4
10+
--> $DIR/needless_doc_main.rs:14:4
11+
|
12+
LL | /// fn main() -> () {
13+
| ^^^^^^^^^^^^^^^^^^
14+
15+
error: needless `fn main` in doctest
16+
--> $DIR/needless_doc_main.rs:21:4
1117
|
1218
LL | /// fn main() {
1319
| ^^^^^^^^^^^^
1420

1521
error: needless `fn main` in doctest
16-
--> $DIR/needless_doc_main.rs:23:4
22+
--> $DIR/needless_doc_main.rs:28:4
1723
|
1824
LL | /// fn main() {
1925
| ^^^^^^^^^^^^
2026

21-
error: aborting due to 3 previous errors
27+
error: aborting due to 4 previous errors
2228

0 commit comments

Comments
 (0)