| //! 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::registry::Registry; |
| use rustc_errors::translation::{Translator, to_fluent_args}; |
| use rustc_errors::{Applicability, DiagCtxt, DiagInner}; |
| 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}; |
| |
| 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); |
| 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, |
| ) { |
| let buffer = Arc::new(Lock::new(Buffer::default())); |
| let translator = rustc_driver::default_translator(); |
| let emitter = BufferEmitter { buffer: Arc::clone(&buffer), translator }; |
| |
| let sm = Arc::new(SourceMap::new(FilePathMapping::empty())); |
| let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings(); |
| let source = dox[code_block.code] |
| .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; |
| }; |
| |
| let empty_block = code_block.lang_string == Default::default() && code_block.is_fenced; |
| let is_ignore = code_block.lang_string.ignore != markdown::Ignore::None; |
| |
| // The span and whether it is precise or not. |
| let (sp, 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), |
| }; |
| |
| let msg = if buffer.has_errors { |
| "could not parse code block as Rust code" |
| } else { |
| "Rust code block is empty" |
| }; |
| |
| // 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.node_span_lint(crate::lint::INVALID_RUST_CODEBLOCKS, hir_id, sp, |lint| { |
| lint.primary_message(msg); |
| |
| 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 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( |
| sp.from_inner(InnerSpan::new(0, 3)), |
| format!("{explanation}: ```text"), |
| ); |
| } else if empty_block { |
| lint.span_suggestion( |
| sp.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()); |
| } |
| }); |
| } |
| |
| #[derive(Default)] |
| struct Buffer { |
| messages: Vec<String>, |
| has_errors: bool, |
| } |
| |
| struct BufferEmitter { |
| buffer: Arc<Lock<Buffer>>, |
| translator: Translator, |
| } |
| |
| impl Emitter for BufferEmitter { |
| fn emit_diagnostic(&mut self, diag: DiagInner, _registry: &Registry) { |
| let mut buffer = self.buffer.borrow_mut(); |
| |
| let fluent_args = to_fluent_args(diag.args.iter()); |
| let translated_main_message = self |
| .translator |
| .translate_message(&diag.messages[0].0, &fluent_args) |
| .unwrap_or_else(|e| panic!("{e}")); |
| |
| 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 |
| } |
| |
| fn translator(&self) -> &Translator { |
| &self.translator |
| } |
| } |