1 //! Validates syntax inside Rust code blocks (\`\`\`rust).
2 use rustc_data_structures
::sync
::{Lock, Lrc}
;
3 use rustc_errors
::{emitter::Emitter, Applicability, Diagnostic, Handler, LazyFallbackBundle}
;
4 use rustc_middle
::lint
::LintDiagnosticBuilder
;
5 use rustc_parse
::parse_stream_from_source_str
;
6 use rustc_session
::parse
::ParseSess
;
7 use rustc_span
::hygiene
::{AstPass, ExpnData, ExpnKind, LocalExpnId}
;
8 use rustc_span
::source_map
::{FilePathMapping, SourceMap}
;
9 use rustc_span
::{FileName, InnerSpan, DUMMY_SP}
;
12 use crate::core
::DocContext
;
13 use crate::html
::markdown
::{self, RustCodeBlock}
;
14 use crate::passes
::Pass
;
15 use crate::visit
::DocVisitor
;
17 pub(crate) const CHECK_CODE_BLOCK_SYNTAX
: Pass
= Pass
{
18 name
: "check-code-block-syntax",
19 run
: check_code_block_syntax
,
20 description
: "validates syntax inside Rust code blocks",
23 pub(crate) fn check_code_block_syntax(
25 cx
: &mut DocContext
<'_
>,
27 SyntaxChecker { cx }
.visit_crate(&krate
);
31 struct SyntaxChecker
<'a
, 'tcx
> {
32 cx
: &'a DocContext
<'tcx
>,
35 impl<'a
, 'tcx
> SyntaxChecker
<'a
, 'tcx
> {
36 fn check_rust_syntax(&self, item
: &clean
::Item
, dox
: &str, code_block
: RustCodeBlock
) {
37 let buffer
= Lrc
::new(Lock
::new(Buffer
::default()));
39 rustc_errors
::fallback_fluent_bundle(rustc_errors
::DEFAULT_LOCALE_RESOURCES
, false);
40 let emitter
= BufferEmitter { buffer: Lrc::clone(&buffer), fallback_bundle }
;
42 let sm
= Lrc
::new(SourceMap
::new(FilePathMapping
::empty()));
43 let handler
= Handler
::with_emitter(false, None
, Box
::new(emitter
));
44 let source
= dox
[code_block
.code
].to_owned();
45 let sess
= ParseSess
::with_span_handler(handler
, sm
);
47 let edition
= code_block
.lang_string
.edition
.unwrap_or_else(|| self.cx
.tcx
.sess
.edition());
48 let expn_data
= ExpnData
::default(
49 ExpnKind
::AstPass(AstPass
::TestHarness
),
55 let expn_id
= LocalExpnId
::fresh(expn_data
, self.cx
.tcx
.create_stable_hashing_context());
56 let span
= DUMMY_SP
.fresh_expansion(expn_id
);
58 let is_empty
= rustc_driver
::catch_fatal_errors(|| {
59 parse_stream_from_source_str(
60 FileName
::Custom(String
::from("doctest")),
68 let buffer
= buffer
.borrow();
70 if !buffer
.has_errors
&& !is_empty
{
71 // No errors in a non-empty program.
75 let Some(local_id
) = item
.item_id
.as_def_id().and_then(|x
| x
.as_local())
77 // We don't need to check the syntax for other crates so returning
78 // without doing anything should not be a problem.
82 let hir_id
= self.cx
.tcx
.hir().local_def_id_to_hir_id(local_id
);
83 let empty_block
= code_block
.lang_string
== Default
::default() && code_block
.is_fenced
;
84 let is_ignore
= code_block
.lang_string
.ignore
!= markdown
::Ignore
::None
;
86 // The span and whether it is precise or not.
87 let (sp
, precise_span
) = match super::source_span_for_markdown_range(
93 Some(sp
) => (sp
, true),
94 None
=> (item
.attr_span(self.cx
.tcx
), false),
97 // lambda that will use the lint to start a new diagnostic and add
98 // a suggestion to it when needed.
99 let diag_builder
= |lint
: LintDiagnosticBuilder
<'_
, ()>| {
100 let explanation
= if is_ignore
{
101 "`ignore` code blocks require valid Rust code for syntax highlighting; \
102 mark blocks that do not contain Rust code as text"
104 "mark blocks that do not contain Rust code as text"
106 let msg
= if buffer
.has_errors
{
107 "could not parse code block as Rust code"
109 "Rust code block is empty"
111 let mut diag
= lint
.build(msg
);
115 // giving an accurate suggestion is hard because `ignore` might not have come first in the list.
116 // just give a `help` instead.
118 sp
.from_inner(InnerSpan
::new(0, 3)),
119 &format
!("{}: ```text", explanation
),
121 } else if empty_block
{
122 diag
.span_suggestion(
123 sp
.from_inner(InnerSpan
::new(0, 3)).shrink_to_hi(),
126 Applicability
::MachineApplicable
,
129 } else if empty_block
|| is_ignore
{
130 diag
.help(&format
!("{}: ```text", explanation
));
133 // FIXME(#67563): Provide more context for these errors by displaying the spans inline.
134 for message
in buffer
.messages
.iter() {
141 // Finally build and emit the completed diagnostic.
142 // All points of divergence have been handled earlier so this can be
143 // done the same way whether the span is precise or not.
144 self.cx
.tcx
.struct_span_lint_hir(
145 crate::lint
::INVALID_RUST_CODEBLOCKS
,
153 impl<'a
, 'tcx
> DocVisitor
for SyntaxChecker
<'a
, 'tcx
> {
154 fn visit_item(&mut self, item
: &clean
::Item
) {
155 if let Some(dox
) = &item
.attrs
.collapsed_doc_value() {
156 let sp
= item
.attr_span(self.cx
.tcx
);
157 let extra
= crate::html
::markdown
::ExtraInfo
::new_did(
159 item
.item_id
.expect_def_id(),
162 for code_block
in markdown
::rust_code_blocks(dox
, &extra
) {
163 self.check_rust_syntax(item
, dox
, code_block
);
167 self.visit_item_recur(item
)
173 messages
: Vec
<String
>,
177 struct BufferEmitter
{
178 buffer
: Lrc
<Lock
<Buffer
>>,
179 fallback_bundle
: LazyFallbackBundle
,
182 impl Emitter
for BufferEmitter
{
183 fn emit_diagnostic(&mut self, diag
: &Diagnostic
) {
184 let mut buffer
= self.buffer
.borrow_mut();
185 // FIXME(davidtwco): need to support translation here eventually
186 buffer
.messages
.push(format
!("error from rustc: {}", diag
.message
[0].0.expect_str()));
188 buffer
.has_errors
= true;
192 fn source_map(&self) -> Option
<&Lrc
<SourceMap
>> {
196 fn fluent_bundle(&self) -> Option
<&Lrc
<rustc_errors
::FluentBundle
>> {
200 fn fallback_fluent_bundle(&self) -> &rustc_errors
::FluentBundle
{
201 &**self.fallback_bundle