Skip to main content

rustdoc/passes/lint/
check_code_block_syntax.rs

1//! Validates syntax inside Rust code blocks (\`\`\`rust).
2
3use 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                    // Giving an accurate suggestion is hard because `ignore` might not have come
71                    // first in the list. Just give a `help` instead.
72                    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            // FIXME(#67563): Provide more context for these errors by displaying the spans inline.
89            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        // No errors in a non-empty program.
129        return;
130    }
131
132    let Some(local_id) = item.item_id.as_def_id().and_then(|x| x.as_local()) else {
133        // We don't need to check the syntax for other crates so returning
134        // without doing anything should not be a problem.
135        return;
136    };
137
138    // The span and whether it is precise or not.
139    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    // Finally build and emit the completed diagnostic.
150    // All points of divergence have been handled earlier so this can be
151    // done the same way whether the span is precise or not.
152    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}