Skip to content

Commit 40123a1

Browse files
committed
Auto merge of #54349 - GuillaumeGomez:no-example-lint, r=QuietMisdreavus
[rustdoc] Add lint for doc without codeblocks Fixes #53805. r? @QuietMisdreavus
2 parents af204b1 + 26479c4 commit 40123a1

File tree

7 files changed

+142
-29
lines changed

7 files changed

+142
-29
lines changed

src/librustc/lint/builtin.rs

+7
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,12 @@ declare_lint! {
312312
"warn about documentation intra links resolution failure"
313313
}
314314

315+
declare_lint! {
316+
pub MISSING_DOC_CODE_EXAMPLES,
317+
Allow,
318+
"warn about missing code example in an item's documentation"
319+
}
320+
315321
declare_lint! {
316322
pub WHERE_CLAUSES_OBJECT_SAFETY,
317323
Warn,
@@ -408,6 +414,7 @@ impl LintPass for HardwiredLints {
408414
DUPLICATE_ASSOCIATED_TYPE_BINDINGS,
409415
DUPLICATE_MACRO_EXPORTS,
410416
INTRA_DOC_LINK_RESOLUTION_FAILURE,
417+
MISSING_DOC_CODE_EXAMPLES,
411418
WHERE_CLAUSES_OBJECT_SAFETY,
412419
PROC_MACRO_DERIVE_RESOLUTION_FALLBACK,
413420
MACRO_USE_EXTERN_CRATE,

src/librustdoc/core.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -348,12 +348,14 @@ pub fn run_core(search_paths: SearchPaths,
348348
let intra_link_resolution_failure_name = lint::builtin::INTRA_DOC_LINK_RESOLUTION_FAILURE.name;
349349
let warnings_lint_name = lint::builtin::WARNINGS.name;
350350
let missing_docs = rustc_lint::builtin::MISSING_DOCS.name;
351+
let missing_doc_example = rustc_lint::builtin::MISSING_DOC_CODE_EXAMPLES.name;
351352

352353
// In addition to those specific lints, we also need to whitelist those given through
353354
// command line, otherwise they'll get ignored and we don't want that.
354355
let mut whitelisted_lints = vec![warnings_lint_name.to_owned(),
355356
intra_link_resolution_failure_name.to_owned(),
356-
missing_docs.to_owned()];
357+
missing_docs.to_owned(),
358+
missing_doc_example.to_owned()];
357359

358360
whitelisted_lints.extend(cmd_lints.iter().map(|(lint, _)| lint).cloned());
359361

src/librustdoc/html/markdown.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -532,8 +532,10 @@ impl fmt::Display for TestableCodeError {
532532
}
533533
}
534534

535-
pub fn find_testable_code(
536-
doc: &str, tests: &mut test::Collector, error_codes: ErrorCodes,
535+
pub fn find_testable_code<T: test::Tester>(
536+
doc: &str,
537+
tests: &mut T,
538+
error_codes: ErrorCodes,
537539
) -> Result<(), TestableCodeError> {
538540
let mut parser = Parser::new(doc);
539541
let mut prev_offset = 0;

src/librustdoc/passes/collect_intra_doc_links.rs

+47-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ use std::ops::Range;
2424

2525
use core::DocContext;
2626
use fold::DocFolder;
27-
use html::markdown::markdown_links;
27+
use html::markdown::{find_testable_code, markdown_links, ErrorCodes, LangString};
28+
2829
use passes::Pass;
2930

3031
pub const COLLECT_INTRA_DOC_LINKS: Pass =
@@ -56,13 +57,15 @@ enum PathKind {
5657
struct LinkCollector<'a, 'tcx: 'a, 'rcx: 'a, 'cstore: 'rcx> {
5758
cx: &'a DocContext<'a, 'tcx, 'rcx, 'cstore>,
5859
mod_ids: Vec<NodeId>,
60+
is_nightly_build: bool,
5961
}
6062

6163
impl<'a, 'tcx, 'rcx, 'cstore> LinkCollector<'a, 'tcx, 'rcx, 'cstore> {
6264
fn new(cx: &'a DocContext<'a, 'tcx, 'rcx, 'cstore>) -> Self {
6365
LinkCollector {
6466
cx,
6567
mod_ids: Vec::new(),
68+
is_nightly_build: UnstableFeatures::from_environment().is_nightly_build(),
6669
}
6770
}
6871

@@ -211,6 +214,43 @@ impl<'a, 'tcx, 'rcx, 'cstore> LinkCollector<'a, 'tcx, 'rcx, 'cstore> {
211214
}
212215
}
213216

217+
fn look_for_tests<'a, 'tcx: 'a, 'rcx: 'a, 'cstore: 'rcx>(
218+
cx: &'a DocContext<'a, 'tcx, 'rcx, 'cstore>,
219+
dox: &str,
220+
item: &Item,
221+
) {
222+
if (item.is_mod() && cx.tcx.hir.as_local_node_id(item.def_id).is_none()) ||
223+
cx.as_local_node_id(item.def_id).is_none() {
224+
// If non-local, no need to check anything.
225+
return;
226+
}
227+
228+
struct Tests {
229+
found_tests: usize,
230+
}
231+
232+
impl ::test::Tester for Tests {
233+
fn add_test(&mut self, _: String, _: LangString, _: usize) {
234+
self.found_tests += 1;
235+
}
236+
}
237+
238+
let mut tests = Tests {
239+
found_tests: 0,
240+
};
241+
242+
if find_testable_code(&dox, &mut tests, ErrorCodes::No).is_ok() {
243+
if tests.found_tests == 0 {
244+
let mut diag = cx.tcx.struct_span_lint_node(
245+
lint::builtin::MISSING_DOC_CODE_EXAMPLES,
246+
NodeId::new(0),
247+
span_of_attrs(&item.attrs),
248+
"Missing code example in this documentation");
249+
diag.emit();
250+
}
251+
}
252+
}
253+
214254
impl<'a, 'tcx, 'rcx, 'cstore> DocFolder for LinkCollector<'a, 'tcx, 'rcx, 'cstore> {
215255
fn fold_item(&mut self, mut item: Item) -> Option<Item> {
216256
let item_node_id = if item.is_mod() {
@@ -273,6 +313,12 @@ impl<'a, 'tcx, 'rcx, 'cstore> DocFolder for LinkCollector<'a, 'tcx, 'rcx, 'cstor
273313
let cx = self.cx;
274314
let dox = item.attrs.collapsed_doc_value().unwrap_or_else(String::new);
275315

316+
look_for_tests(&cx, &dox, &item);
317+
318+
if !self.is_nightly_build {
319+
return None;
320+
}
321+
276322
for (ori_link, link_range) in markdown_links(&dox) {
277323
// bail early for real links
278324
if ori_link.contains('/') {

src/librustdoc/test.rs

+35-25
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,14 @@ fn partition_source(s: &str) -> (String, String) {
465465
(before, after)
466466
}
467467

468+
pub trait Tester {
469+
fn add_test(&mut self, test: String, config: LangString, line: usize);
470+
fn get_line(&self) -> usize {
471+
0
472+
}
473+
fn register_header(&mut self, _name: &str, _level: u32) {}
474+
}
475+
468476
pub struct Collector {
469477
pub tests: Vec<testing::TestDescAndFn>,
470478

@@ -533,7 +541,31 @@ impl Collector {
533541
format!("{} - {} (line {})", filename, self.names.join("::"), line)
534542
}
535543

536-
pub fn add_test(&mut self, test: String, config: LangString, line: usize) {
544+
pub fn set_position(&mut self, position: Span) {
545+
self.position = position;
546+
}
547+
548+
fn get_filename(&self) -> FileName {
549+
if let Some(ref source_map) = self.source_map {
550+
let filename = source_map.span_to_filename(self.position);
551+
if let FileName::Real(ref filename) = filename {
552+
if let Ok(cur_dir) = env::current_dir() {
553+
if let Ok(path) = filename.strip_prefix(&cur_dir) {
554+
return path.to_owned().into();
555+
}
556+
}
557+
}
558+
filename
559+
} else if let Some(ref filename) = self.filename {
560+
filename.clone().into()
561+
} else {
562+
FileName::Custom("input".to_owned())
563+
}
564+
}
565+
}
566+
567+
impl Tester for Collector {
568+
fn add_test(&mut self, test: String, config: LangString, line: usize) {
537569
let filename = self.get_filename();
538570
let name = self.generate_name(line, &filename);
539571
let cfgs = self.cfgs.clone();
@@ -587,7 +619,7 @@ impl Collector {
587619
});
588620
}
589621

590-
pub fn get_line(&self) -> usize {
622+
fn get_line(&self) -> usize {
591623
if let Some(ref source_map) = self.source_map {
592624
let line = self.position.lo().to_usize();
593625
let line = source_map.lookup_char_pos(BytePos(line as u32)).line;
@@ -597,29 +629,7 @@ impl Collector {
597629
}
598630
}
599631

600-
pub fn set_position(&mut self, position: Span) {
601-
self.position = position;
602-
}
603-
604-
fn get_filename(&self) -> FileName {
605-
if let Some(ref source_map) = self.source_map {
606-
let filename = source_map.span_to_filename(self.position);
607-
if let FileName::Real(ref filename) = filename {
608-
if let Ok(cur_dir) = env::current_dir() {
609-
if let Ok(path) = filename.strip_prefix(&cur_dir) {
610-
return path.to_owned().into();
611-
}
612-
}
613-
}
614-
filename
615-
} else if let Some(ref filename) = self.filename {
616-
filename.clone().into()
617-
} else {
618-
FileName::Custom("input".to_owned())
619-
}
620-
}
621-
622-
pub fn register_header(&mut self, name: &str, level: u32) {
632+
fn register_header(&mut self, name: &str, level: u32) {
623633
if self.use_headers {
624634
// we use these headings as test names, so it's good if
625635
// they're valid identifiers.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2018 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
#![deny(missing_doc_code_examples)]
12+
13+
/// Some docs.
14+
pub struct Foo;
15+
16+
/// And then, the princess died.
17+
pub mod foo {
18+
/// Or maybe not because she saved herself!
19+
pub fn bar() {}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
error: Missing code example in this documentation
2+
|
3+
note: lint level defined here
4+
--> $DIR/doc-without-codeblock.rs:11:9
5+
|
6+
LL | #![deny(missing_doc_code_examples)]
7+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
8+
9+
error: Missing code example in this documentation
10+
--> $DIR/doc-without-codeblock.rs:13:1
11+
|
12+
LL | /// Some docs.
13+
| ^^^^^^^^^^^^^^
14+
15+
error: Missing code example in this documentation
16+
--> $DIR/doc-without-codeblock.rs:16:1
17+
|
18+
LL | /// And then, the princess died.
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
20+
21+
error: Missing code example in this documentation
22+
--> $DIR/doc-without-codeblock.rs:18:5
23+
|
24+
LL | /// Or maybe not because she saved herself!
25+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26+

0 commit comments

Comments
 (0)