]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/passes/check_code_block_syntax.rs
Rollup merge of #84484 - jyn514:check-tools, r=Mark-Simulacrum
[rust.git] / src / librustdoc / passes / check_code_block_syntax.rs
1 use rustc_data_structures::sync::{Lock, Lrc};
2 use rustc_errors::{emitter::Emitter, Applicability, Diagnostic, Handler};
3 use rustc_parse::parse_stream_from_source_str;
4 use rustc_session::parse::ParseSess;
5 use rustc_span::source_map::{FilePathMapping, SourceMap};
6 use rustc_span::{FileName, InnerSpan};
7
8 use crate::clean;
9 use crate::core::DocContext;
10 use crate::fold::DocFolder;
11 use crate::html::markdown::{self, RustCodeBlock};
12 use crate::passes::Pass;
13
14 crate const CHECK_CODE_BLOCK_SYNTAX: Pass = Pass {
15     name: "check-code-block-syntax",
16     run: check_code_block_syntax,
17     description: "validates syntax inside Rust code blocks",
18 };
19
20 crate fn check_code_block_syntax(krate: clean::Crate, cx: &mut DocContext<'_>) -> clean::Crate {
21     SyntaxChecker { cx }.fold_crate(krate)
22 }
23
24 struct SyntaxChecker<'a, 'tcx> {
25     cx: &'a DocContext<'tcx>,
26 }
27
28 impl<'a, 'tcx> SyntaxChecker<'a, 'tcx> {
29     fn check_rust_syntax(&self, item: &clean::Item, dox: &str, code_block: RustCodeBlock) {
30         let buffer = Lrc::new(Lock::new(Buffer::default()));
31         let emitter = BufferEmitter { buffer: Lrc::clone(&buffer) };
32
33         let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
34         let handler = Handler::with_emitter(false, None, Box::new(emitter));
35         let source = dox[code_block.code].to_owned();
36         let sess = ParseSess::with_span_handler(handler, sm);
37
38         let is_empty = rustc_driver::catch_fatal_errors(|| {
39             parse_stream_from_source_str(
40                 FileName::Custom(String::from("doctest")),
41                 source,
42                 &sess,
43                 None,
44             )
45             .is_empty()
46         })
47         .unwrap_or(false);
48         let buffer = buffer.borrow();
49
50         if buffer.has_errors || is_empty {
51             let mut diag = if let Some(sp) = super::source_span_for_markdown_range(
52                 self.cx.tcx,
53                 &dox,
54                 &code_block.range,
55                 &item.attrs,
56             ) {
57                 let (warning_message, suggest_using_text) = if buffer.has_errors {
58                     ("could not parse code block as Rust code", true)
59                 } else {
60                     ("Rust code block is empty", false)
61                 };
62
63                 let mut diag = self.cx.sess().struct_span_warn(sp, warning_message);
64
65                 if code_block.syntax.is_none() && code_block.is_fenced {
66                     let sp = sp.from_inner(InnerSpan::new(0, 3));
67                     diag.span_suggestion(
68                         sp,
69                         "mark blocks that do not contain Rust code as text",
70                         String::from("```text"),
71                         Applicability::MachineApplicable,
72                     );
73                 } else if suggest_using_text && code_block.is_ignore {
74                     let sp = sp.from_inner(InnerSpan::new(0, 3));
75                     diag.span_suggestion(
76                         sp,
77                         "`ignore` code blocks require valid Rust code for syntax highlighting. \
78                          Mark blocks that do not contain Rust code as text",
79                         String::from("```text,"),
80                         Applicability::MachineApplicable,
81                     );
82                 }
83
84                 diag
85             } else {
86                 // We couldn't calculate the span of the markdown block that had the error, so our
87                 // diagnostics are going to be a bit lacking.
88                 let mut diag = self.cx.sess().struct_span_warn(
89                     item.attr_span(self.cx.tcx),
90                     "doc comment contains an invalid Rust code block",
91                 );
92
93                 if code_block.syntax.is_none() && code_block.is_fenced {
94                     diag.help("mark blocks that do not contain Rust code as text: ```text");
95                 }
96
97                 diag
98             };
99
100             // FIXME(#67563): Provide more context for these errors by displaying the spans inline.
101             for message in buffer.messages.iter() {
102                 diag.note(&message);
103             }
104
105             diag.emit();
106         }
107     }
108 }
109
110 impl<'a, 'tcx> DocFolder for SyntaxChecker<'a, 'tcx> {
111     fn fold_item(&mut self, item: clean::Item) -> Option<clean::Item> {
112         if let Some(dox) = &item.attrs.collapsed_doc_value() {
113             let sp = item.attr_span(self.cx.tcx);
114             let extra = crate::html::markdown::ExtraInfo::new_did(self.cx.tcx, item.def_id, sp);
115             for code_block in markdown::rust_code_blocks(&dox, &extra) {
116                 self.check_rust_syntax(&item, &dox, code_block);
117             }
118         }
119
120         Some(self.fold_item_recur(item))
121     }
122 }
123
124 #[derive(Default)]
125 struct Buffer {
126     messages: Vec<String>,
127     has_errors: bool,
128 }
129
130 struct BufferEmitter {
131     buffer: Lrc<Lock<Buffer>>,
132 }
133
134 impl Emitter for BufferEmitter {
135     fn emit_diagnostic(&mut self, diag: &Diagnostic) {
136         let mut buffer = self.buffer.borrow_mut();
137         buffer.messages.push(format!("error from rustc: {}", diag.message[0].0));
138         if diag.is_error() {
139             buffer.has_errors = true;
140         }
141     }
142
143     fn source_map(&self) -> Option<&Lrc<SourceMap>> {
144         None
145     }
146 }