| //! Validates syntax inside Rust code blocks (\`\`\`rust). | |
| use std::borrow::Cow; | |
| use std::sync::Arc; | |
| use rustc_data_structures::sync::Lock; | |
| use rustc_errors::emitter::Emitter; | |
| use rustc_errors::formatting::format_diag_message; | |
| use rustc_errors::{Applicability, Diag, DiagCtxt, DiagCtxtHandle, DiagInner, Diagnostic, Level}; | |
| use rustc_parse::{source_str_to_stream, unwrap_or_emit_fatal}; | |
| use rustc_resolve::rustdoc::source_span_for_markdown_range; | |
| use rustc_session::parse::ParseSess; | |
| use rustc_span::hygiene::{AstPass, ExpnData, ExpnKind, LocalExpnId, Transparency}; | |
| use rustc_span::source_map::{FilePathMapping, SourceMap}; | |
| use rustc_span::{DUMMY_SP, FileName, InnerSpan, Span}; | |
| use crate::clean; | |
| use crate::core::DocContext; | |
| use crate::html::markdown::{self, RustCodeBlock}; | |
| pub(crate) fn visit_item(cx: &DocContext<'_>, item: &clean::Item, dox: &str) { | |
| if let Some(def_id) = item.item_id.as_local_def_id() { | |
| let sp = item.attr_span(cx.tcx); | |
| let extra = crate::html::markdown::ExtraInfo::new(cx.tcx, def_id, sp, None); | |
| for code_block in markdown::rust_code_blocks(dox, &extra) { | |
| check_rust_syntax(cx, item, dox, code_block); | |
| } | |
| } | |
| } | |
| fn check_rust_syntax( | |
| cx: &DocContext<'_>, | |
| item: &clean::Item, | |
| dox: &str, | |
| code_block: RustCodeBlock, | |
| ) { | |
| struct CodeblockError<'a> { | |
| buffer: &'a Buffer, | |
| code_block: RustCodeBlock, | |
| span: Span, | |
| is_precise_span: bool, | |
| } | |
| impl<'a, 'b> Diagnostic<'a, ()> for CodeblockError<'b> { | |
| fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> { | |
| let Self { buffer, code_block, span, is_precise_span } = self; | |
| let mut lint = Diag::new( | |
| dcx, | |
| level, | |
| if buffer.has_errors { | |
| "could not parse code block as Rust code" | |
| } else { | |
| "Rust code block is empty" | |
| }, | |
| ); | |
| let empty_block = code_block.lang_string == Default::default() && code_block.is_fenced; | |
| let is_ignore = code_block.lang_string.ignore != markdown::Ignore::None; | |
| let explanation = if is_ignore { | |
| "`ignore` code blocks require valid Rust code for syntax highlighting; \ | |
| mark blocks that do not contain Rust code as text" | |
| } else { | |
| "mark blocks that do not contain Rust code as text" | |
| }; | |
| if is_precise_span { | |
| if is_ignore { | |
| // Giving an accurate suggestion is hard because `ignore` might not have come | |
| // first in the list. Just give a `help` instead. | |
| lint.span_help( | |
| span.from_inner(InnerSpan::new(0, 3)), | |
| format!("{explanation}: ```text"), | |
| ); | |
| } else if empty_block { | |
| lint.span_suggestion( | |
| span.from_inner(InnerSpan::new(0, 3)).shrink_to_hi(), | |
| explanation, | |
| "text", | |
| Applicability::MachineApplicable, | |
| ); | |
| } | |
| } else if empty_block || is_ignore { | |
| lint.help(format!("{explanation}: ```text")); | |
| } | |
| // FIXME(#67563): Provide more context for these errors by displaying the spans inline. | |
| for message in buffer.messages.iter() { | |
| lint.note(message.clone()); | |
| } | |
| lint | |
| } | |
| } | |
| let buffer = Arc::new(Lock::new(Buffer::default())); | |
| let emitter = BufferEmitter { buffer: Arc::clone(&buffer) }; | |
| let sm = Arc::new(SourceMap::new(FilePathMapping::empty())); | |
| let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings(); | |
| let source = dox[code_block.code.clone()] | |
| .lines() | |
| .map(|line| crate::html::markdown::map_line(line).for_code()) | |
| .intersperse(Cow::Borrowed("\n")) | |
| .collect::<String>(); | |
| let psess = ParseSess::with_dcx(dcx, sm); | |
| let edition = code_block.lang_string.edition.unwrap_or_else(|| cx.tcx.sess.edition()); | |
| let expn_data = | |
| ExpnData::default(ExpnKind::AstPass(AstPass::TestHarness), DUMMY_SP, edition, None, None); | |
| let expn_id = cx.tcx.with_stable_hashing_context(|hcx| LocalExpnId::fresh(expn_data, hcx)); | |
| let span = DUMMY_SP.apply_mark(expn_id.to_expn_id(), Transparency::Transparent); | |
| let is_empty = rustc_driver::catch_fatal_errors(|| { | |
| unwrap_or_emit_fatal(source_str_to_stream( | |
| &psess, | |
| FileName::Custom(String::from("doctest")), | |
| source, | |
| Some(span), | |
| )) | |
| .is_empty() | |
| }) | |
| .unwrap_or(false); | |
| let buffer = buffer.borrow(); | |
| if !buffer.has_errors && !is_empty { | |
| // No errors in a non-empty program. | |
| return; | |
| } | |
| let Some(local_id) = item.item_id.as_def_id().and_then(|x| x.as_local()) else { | |
| // We don't need to check the syntax for other crates so returning | |
| // without doing anything should not be a problem. | |
| return; | |
| }; | |
| // The span and whether it is precise or not. | |
| let (span, is_precise_span) = match source_span_for_markdown_range( | |
| cx.tcx, | |
| dox, | |
| &code_block.range, | |
| &item.attrs.doc_strings, | |
| ) { | |
| Some((sp, _)) => (sp, true), | |
| None => (item.attr_span(cx.tcx), false), | |
| }; | |
| // Finally build and emit the completed diagnostic. | |
| // All points of divergence have been handled earlier so this can be | |
| // done the same way whether the span is precise or not. | |
| let hir_id = cx.tcx.local_def_id_to_hir_id(local_id); | |
| cx.tcx.emit_node_span_lint( | |
| crate::lint::INVALID_RUST_CODEBLOCKS, | |
| hir_id, | |
| span, | |
| CodeblockError { buffer: &buffer, code_block, span, is_precise_span }, | |
| ); | |
| } | |
| #[derive(Default)] | |
| struct Buffer { | |
| messages: Vec<String>, | |
| has_errors: bool, | |
| } | |
| struct BufferEmitter { | |
| buffer: Arc<Lock<Buffer>>, | |
| } | |
| impl Emitter for BufferEmitter { | |
| fn emit_diagnostic(&mut self, diag: DiagInner) { | |
| let mut buffer = self.buffer.borrow_mut(); | |
| let translated_main_message = format_diag_message(&diag.messages[0].0, &diag.args); | |
| buffer.messages.push(format!("error from rustc: {translated_main_message}")); | |
| if diag.is_error() { | |
| buffer.has_errors = true; | |
| } | |
| } | |
| fn source_map(&self) -> Option<&SourceMap> { | |
| None | |
| } | |
| } |