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::translation::{Translator, to_fluent_args};
9use rustc_errors::{Applicability, DiagCtxt, DiagInner};
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};
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 let buffer = Arc::new(Lock::new(Buffer::default()));
38 let translator = rustc_driver::default_translator();
39 let emitter = BufferEmitter { buffer: Arc::clone(&buffer), translator };
40
41 let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
42 let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
43 let source = dox[code_block.code]
44 .lines()
45 .map(|line| crate::html::markdown::map_line(line).for_code())
46 .intersperse(Cow::Borrowed("\n"))
47 .collect::<String>();
48 let psess = ParseSess::with_dcx(dcx, sm);
49
50 let edition = code_block.lang_string.edition.unwrap_or_else(|| cx.tcx.sess.edition());
51 let expn_data =
52 ExpnData::default(ExpnKind::AstPass(AstPass::TestHarness), DUMMY_SP, edition, None, None);
53 let expn_id = cx.tcx.with_stable_hashing_context(|hcx| LocalExpnId::fresh(expn_data, hcx));
54 let span = DUMMY_SP.apply_mark(expn_id.to_expn_id(), Transparency::Transparent);
55
56 let is_empty = rustc_driver::catch_fatal_errors(|| {
57 unwrap_or_emit_fatal(source_str_to_stream(
58 &psess,
59 FileName::Custom(String::from("doctest")),
60 source,
61 Some(span),
62 ))
63 .is_empty()
64 })
65 .unwrap_or(false);
66 let buffer = buffer.borrow();
67
68 if !buffer.has_errors && !is_empty {
69 return;
71 }
72
73 let Some(local_id) = item.item_id.as_def_id().and_then(|x| x.as_local()) else {
74 return;
77 };
78
79 let empty_block = code_block.lang_string == Default::default() && code_block.is_fenced;
80 let is_ignore = code_block.lang_string.ignore != markdown::Ignore::None;
81
82 let (sp, precise_span) = match source_span_for_markdown_range(
84 cx.tcx,
85 dox,
86 &code_block.range,
87 &item.attrs.doc_strings,
88 ) {
89 Some((sp, _)) => (sp, true),
90 None => (item.attr_span(cx.tcx), false),
91 };
92
93 let msg = if buffer.has_errors {
94 "could not parse code block as Rust code"
95 } else {
96 "Rust code block is empty"
97 };
98
99 let hir_id = cx.tcx.local_def_id_to_hir_id(local_id);
103 cx.tcx.node_span_lint(crate::lint::INVALID_RUST_CODEBLOCKS, hir_id, sp, |lint| {
104 lint.primary_message(msg);
105
106 let explanation = if is_ignore {
107 "`ignore` code blocks require valid Rust code for syntax highlighting; \
108 mark blocks that do not contain Rust code as text"
109 } else {
110 "mark blocks that do not contain Rust code as text"
111 };
112
113 if precise_span {
114 if is_ignore {
115 lint.span_help(
118 sp.from_inner(InnerSpan::new(0, 3)),
119 format!("{explanation}: ```text"),
120 );
121 } else if empty_block {
122 lint.span_suggestion(
123 sp.from_inner(InnerSpan::new(0, 3)).shrink_to_hi(),
124 explanation,
125 "text",
126 Applicability::MachineApplicable,
127 );
128 }
129 } else if empty_block || is_ignore {
130 lint.help(format!("{explanation}: ```text"));
131 }
132
133 for message in buffer.messages.iter() {
135 lint.note(message.clone());
136 }
137 });
138}
139
140#[derive(Default)]
141struct Buffer {
142 messages: Vec<String>,
143 has_errors: bool,
144}
145
146struct BufferEmitter {
147 buffer: Arc<Lock<Buffer>>,
148 translator: Translator,
149}
150
151impl Emitter for BufferEmitter {
152 fn emit_diagnostic(&mut self, diag: DiagInner) {
153 let mut buffer = self.buffer.borrow_mut();
154
155 let fluent_args = to_fluent_args(diag.args.iter());
156 let translated_main_message = self
157 .translator
158 .translate_message(&diag.messages[0].0, &fluent_args)
159 .unwrap_or_else(|e| panic!("{e}"));
160
161 buffer.messages.push(format!("error from rustc: {translated_main_message}"));
162 if diag.is_error() {
163 buffer.has_errors = true;
164 }
165 }
166
167 fn source_map(&self) -> Option<&SourceMap> {
168 None
169 }
170
171 fn translator(&self) -> &Translator {
172 &self.translator
173 }
174}