]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/passes/check_code_block_syntax.rs
Rollup merge of #104317 - RalfJung:ctfe-error-reporting, r=oli-obk
[rust.git] / src / librustdoc / passes / check_code_block_syntax.rs
1 //! Validates syntax inside Rust code blocks (\`\`\`rust).
2 use rustc_data_structures::sync::{Lock, Lrc};
3 use rustc_errors::{
4     emitter::Emitter,
5     translation::{to_fluent_args, Translate},
6     Applicability, Diagnostic, Handler, LazyFallbackBundle,
7 };
8 use rustc_parse::parse_stream_from_source_str;
9 use rustc_session::parse::ParseSess;
10 use rustc_span::hygiene::{AstPass, ExpnData, ExpnKind, LocalExpnId};
11 use rustc_span::source_map::{FilePathMapping, SourceMap};
12 use rustc_span::{FileName, InnerSpan, DUMMY_SP};
13
14 use crate::clean;
15 use crate::core::DocContext;
16 use crate::html::markdown::{self, RustCodeBlock};
17 use crate::passes::Pass;
18 use crate::visit::DocVisitor;
19
20 pub(crate) const CHECK_CODE_BLOCK_SYNTAX: Pass = Pass {
21     name: "check-code-block-syntax",
22     run: check_code_block_syntax,
23     description: "validates syntax inside Rust code blocks",
24 };
25
26 pub(crate) fn check_code_block_syntax(
27     krate: clean::Crate,
28     cx: &mut DocContext<'_>,
29 ) -> clean::Crate {
30     SyntaxChecker { cx }.visit_crate(&krate);
31     krate
32 }
33
34 struct SyntaxChecker<'a, 'tcx> {
35     cx: &'a DocContext<'tcx>,
36 }
37
38 impl<'a, 'tcx> SyntaxChecker<'a, 'tcx> {
39     fn check_rust_syntax(&self, item: &clean::Item, dox: &str, code_block: RustCodeBlock) {
40         let buffer = Lrc::new(Lock::new(Buffer::default()));
41         let fallback_bundle =
42             rustc_errors::fallback_fluent_bundle(rustc_errors::DEFAULT_LOCALE_RESOURCES, false);
43         let emitter = BufferEmitter { buffer: Lrc::clone(&buffer), fallback_bundle };
44
45         let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
46         let handler = Handler::with_emitter(false, None, Box::new(emitter));
47         let source = dox[code_block.code].to_owned();
48         let sess = ParseSess::with_span_handler(handler, sm);
49
50         let edition = code_block.lang_string.edition.unwrap_or_else(|| self.cx.tcx.sess.edition());
51         let expn_data = ExpnData::default(
52             ExpnKind::AstPass(AstPass::TestHarness),
53             DUMMY_SP,
54             edition,
55             None,
56             None,
57         );
58         let expn_id =
59             self.cx.tcx.with_stable_hashing_context(|hcx| LocalExpnId::fresh(expn_data, hcx));
60         let span = DUMMY_SP.fresh_expansion(expn_id);
61
62         let is_empty = rustc_driver::catch_fatal_errors(|| {
63             parse_stream_from_source_str(
64                 FileName::Custom(String::from("doctest")),
65                 source,
66                 &sess,
67                 Some(span),
68             )
69             .is_empty()
70         })
71         .unwrap_or(false);
72         let buffer = buffer.borrow();
73
74         if !buffer.has_errors && !is_empty {
75             // No errors in a non-empty program.
76             return;
77         }
78
79         let Some(local_id) = item.item_id.as_def_id().and_then(|x| x.as_local())
80         else {
81             // We don't need to check the syntax for other crates so returning
82             // without doing anything should not be a problem.
83             return;
84         };
85
86         let hir_id = self.cx.tcx.hir().local_def_id_to_hir_id(local_id);
87         let empty_block = code_block.lang_string == Default::default() && code_block.is_fenced;
88         let is_ignore = code_block.lang_string.ignore != markdown::Ignore::None;
89
90         // The span and whether it is precise or not.
91         let (sp, precise_span) = match super::source_span_for_markdown_range(
92             self.cx.tcx,
93             dox,
94             &code_block.range,
95             &item.attrs,
96         ) {
97             Some(sp) => (sp, true),
98             None => (item.attr_span(self.cx.tcx), false),
99         };
100
101         let msg = if buffer.has_errors {
102             "could not parse code block as Rust code"
103         } else {
104             "Rust code block is empty"
105         };
106
107         // Finally build and emit the completed diagnostic.
108         // All points of divergence have been handled earlier so this can be
109         // done the same way whether the span is precise or not.
110         self.cx.tcx.struct_span_lint_hir(
111             crate::lint::INVALID_RUST_CODEBLOCKS,
112             hir_id,
113             sp,
114             msg,
115             |lint| {
116                 let explanation = if is_ignore {
117                     "`ignore` code blocks require valid Rust code for syntax highlighting; \
118                     mark blocks that do not contain Rust code as text"
119                 } else {
120                     "mark blocks that do not contain Rust code as text"
121                 };
122
123                 if precise_span {
124                     if is_ignore {
125                         // giving an accurate suggestion is hard because `ignore` might not have come first in the list.
126                         // just give a `help` instead.
127                         lint.span_help(
128                             sp.from_inner(InnerSpan::new(0, 3)),
129                             &format!("{}: ```text", explanation),
130                         );
131                     } else if empty_block {
132                         lint.span_suggestion(
133                             sp.from_inner(InnerSpan::new(0, 3)).shrink_to_hi(),
134                             explanation,
135                             "text",
136                             Applicability::MachineApplicable,
137                         );
138                     }
139                 } else if empty_block || is_ignore {
140                     lint.help(&format!("{}: ```text", explanation));
141                 }
142
143                 // FIXME(#67563): Provide more context for these errors by displaying the spans inline.
144                 for message in buffer.messages.iter() {
145                     lint.note(message);
146                 }
147
148                 lint
149             },
150         );
151     }
152 }
153
154 impl<'a, 'tcx> DocVisitor for SyntaxChecker<'a, 'tcx> {
155     fn visit_item(&mut self, item: &clean::Item) {
156         if let Some(dox) = &item.attrs.collapsed_doc_value() {
157             let sp = item.attr_span(self.cx.tcx);
158             let extra = crate::html::markdown::ExtraInfo::new_did(
159                 self.cx.tcx,
160                 item.item_id.expect_def_id(),
161                 sp,
162             );
163             for code_block in markdown::rust_code_blocks(dox, &extra) {
164                 self.check_rust_syntax(item, dox, code_block);
165             }
166         }
167
168         self.visit_item_recur(item)
169     }
170 }
171
172 #[derive(Default)]
173 struct Buffer {
174     messages: Vec<String>,
175     has_errors: bool,
176 }
177
178 struct BufferEmitter {
179     buffer: Lrc<Lock<Buffer>>,
180     fallback_bundle: LazyFallbackBundle,
181 }
182
183 impl Translate for BufferEmitter {
184     fn fluent_bundle(&self) -> Option<&Lrc<rustc_errors::FluentBundle>> {
185         None
186     }
187
188     fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle {
189         &**self.fallback_bundle
190     }
191 }
192
193 impl Emitter for BufferEmitter {
194     fn emit_diagnostic(&mut self, diag: &Diagnostic) {
195         let mut buffer = self.buffer.borrow_mut();
196
197         let fluent_args = to_fluent_args(diag.args());
198         let translated_main_message = self.translate_message(&diag.message[0].0, &fluent_args);
199
200         buffer.messages.push(format!("error from rustc: {}", translated_main_message));
201         if diag.is_error() {
202             buffer.has_errors = true;
203         }
204     }
205
206     fn source_map(&self) -> Option<&Lrc<SourceMap>> {
207         None
208     }
209 }