rustdoc/passes/lint/
check_code_block_syntax.rs1use std::borrow::Cow;
4use std::sync::Arc;
5
6use rustc_data_structures::sync::Lock;
7use rustc_errors::emitter::Emitter;
8use rustc_errors::formatting::format_diag_message;
9use rustc_errors::{Applicability, Diag, DiagCtxt, DiagCtxtHandle, DiagInner, Diagnostic, Level};
10use rustc_parse::{source_str_to_stream, unwrap_or_emit_fatal};
11use rustc_resolve::rustdoc::source_span_for_markdown_range;
12use rustc_session::parse::ParseSess;
13use rustc_span::hygiene::{AstPass, ExpnData, ExpnKind, LocalExpnId, Transparency};
14use rustc_span::source_map::{FilePathMapping, SourceMap};
15use rustc_span::{DUMMY_SP, FileName, InnerSpan, Span};
16
17use crate::clean;
18use crate::core::DocContext;
19use crate::html::markdown::{self, RustCodeBlock};
20
21pub(crate) fn visit_item(cx: &DocContext<'_>, item: &clean::Item, dox: &str) {
22 if let Some(def_id) = item.item_id.as_local_def_id() {
23 let sp = item.attr_span(cx.tcx);
24 let extra = crate::html::markdown::ExtraInfo::new(cx.tcx, def_id, sp);
25 for code_block in markdown::rust_code_blocks(dox, &extra) {
26 check_rust_syntax(cx, item, dox, code_block);
27 }
28 }
29}
30
31fn check_rust_syntax(
32 cx: &DocContext<'_>,
33 item: &clean::Item,
34 dox: &str,
35 code_block: RustCodeBlock,
36) {
37 struct CodeblockError<'a> {
38 buffer: &'a Buffer,
39 code_block: RustCodeBlock,
40 span: Span,
41 is_precise_span: bool,
42 }
43
44 impl<'a, 'b> Diagnostic<'a, ()> for CodeblockError<'b> {
45 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> {
46 let Self { buffer, code_block, span, is_precise_span } = self;
47
48 let mut lint = Diag::new(
49 dcx,
50 level,
51 if buffer.has_errors {
52 "could not parse code block as Rust code"
53 } else {
54 "Rust code block is empty"
55 },
56 );
57
58 let empty_block = code_block.lang_string == Default::default() && code_block.is_fenced;
59 let is_ignore = code_block.lang_string.ignore != markdown::Ignore::None;
60
61 let explanation = if is_ignore {
62 "`ignore` code blocks require valid Rust code for syntax highlighting; \
63 mark blocks that do not contain Rust code as text"
64 } else {
65 "mark blocks that do not contain Rust code as text"
66 };
67
68 if is_precise_span {
69 if is_ignore {
70 lint.span_help(
73 span.from_inner(InnerSpan::new(0, 3)),
74 format!("{explanation}: ```text"),
75 );
76 } else if empty_block {
77 lint.span_suggestion(
78 span.from_inner(InnerSpan::new(0, 3)).shrink_to_hi(),
79 explanation,
80 "text",
81 Applicability::MachineApplicable,
82 );
83 }
84 } else if empty_block || is_ignore {
85 lint.help(format!("{explanation}: ```text"));
86 }
87
88 for message in buffer.messages.iter() {
90 lint.note(message.clone());
91 }
92
93 lint
94 }
95 }
96
97 let buffer = Arc::new(Lock::new(Buffer::default()));
98 let emitter = BufferEmitter { buffer: Arc::clone(&buffer) };
99
100 let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
101 let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
102 let source = dox[code_block.code.clone()]
103 .lines()
104 .map(|line| crate::html::markdown::map_line(line).for_code())
105 .intersperse(Cow::Borrowed("\n"))
106 .collect::<String>();
107 let psess = ParseSess::with_dcx(dcx, sm);
108
109 let edition = code_block.lang_string.edition.unwrap_or_else(|| cx.tcx.sess.edition());
110 let expn_data =
111 ExpnData::default(ExpnKind::AstPass(AstPass::TestHarness), DUMMY_SP, edition, None, None);
112 let expn_id = cx.tcx.with_stable_hashing_context(|hcx| LocalExpnId::fresh(expn_data, hcx));
113 let span = DUMMY_SP.apply_mark(expn_id.to_expn_id(), Transparency::Transparent);
114
115 let is_empty = rustc_driver::catch_fatal_errors(|| {
116 unwrap_or_emit_fatal(source_str_to_stream(
117 &psess,
118 FileName::Custom(String::from("doctest")),
119 source,
120 Some(span),
121 ))
122 .is_empty()
123 })
124 .unwrap_or(false);
125 let buffer = buffer.borrow();
126
127 if !buffer.has_errors && !is_empty {
128 return;
130 }
131
132 let Some(local_id) = item.item_id.as_def_id().and_then(|x| x.as_local()) else {
133 return;
136 };
137
138 let (span, is_precise_span) = match source_span_for_markdown_range(
140 cx.tcx,
141 dox,
142 &code_block.range,
143 &item.attrs.doc_strings,
144 ) {
145 Some((sp, _)) => (sp, true),
146 None => (item.attr_span(cx.tcx), false),
147 };
148
149 let hir_id = cx.tcx.local_def_id_to_hir_id(local_id);
153 cx.tcx.emit_node_span_lint(
154 crate::lint::INVALID_RUST_CODEBLOCKS,
155 hir_id,
156 span,
157 CodeblockError { buffer: &buffer, code_block, span, is_precise_span },
158 );
159}
160
161#[derive(Default)]
162struct Buffer {
163 messages: Vec<String>,
164 has_errors: bool,
165}
166
167struct BufferEmitter {
168 buffer: Arc<Lock<Buffer>>,
169}
170
171impl Emitter for BufferEmitter {
172 fn emit_diagnostic(&mut self, diag: DiagInner) {
173 let mut buffer = self.buffer.borrow_mut();
174
175 let translated_main_message = format_diag_message(&diag.messages[0].0, &diag.args);
176
177 buffer.messages.push(format!("error from rustc: {translated_main_message}"));
178 if diag.is_error() {
179 buffer.has_errors = true;
180 }
181 }
182
183 fn source_map(&self) -> Option<&SourceMap> {
184 None
185 }
186}