about summary refs log tree commit diff
path: root/src/librustdoc/passes/check_code_block_syntax.rs
blob: 5c4c975cec051b1c91a2e8b46541ede12fa3bf5b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
use errors::Applicability;
use rustc_parse::lexer::{StringReader as Lexer};
use syntax::token;
use syntax::sess::ParseSess;
use syntax::source_map::FilePathMapping;
use syntax_expand::config::process_configure_mod;
use syntax_pos::{InnerSpan, FileName};

use crate::clean;
use crate::core::DocContext;
use crate::fold::DocFolder;
use crate::html::markdown::{self, RustCodeBlock};
use crate::passes::Pass;

pub const CHECK_CODE_BLOCK_SYNTAX: Pass = Pass {
    name: "check-code-block-syntax",
    pass: check_code_block_syntax,
    description: "validates syntax inside Rust code blocks",
};

pub fn check_code_block_syntax(krate: clean::Crate, cx: &DocContext<'_>) -> clean::Crate {
    SyntaxChecker { cx }.fold_crate(krate)
}

struct SyntaxChecker<'a, 'tcx> {
    cx: &'a DocContext<'tcx>,
}

impl<'a, 'tcx> SyntaxChecker<'a, 'tcx> {
    fn check_rust_syntax(&self, item: &clean::Item, dox: &str, code_block: RustCodeBlock) {
        let sess = ParseSess::new(FilePathMapping::empty(), process_configure_mod);
        let source_file = sess.source_map().new_source_file(
            FileName::Custom(String::from("doctest")),
            dox[code_block.code].to_owned(),
        );

        let validation_status = {
            let mut has_syntax_errors = false;
            let mut only_whitespace = true;
            // even if there is a syntax error, we need to run the lexer over the whole file
            let mut lexer = Lexer::new(&sess, source_file, None);
            loop  {
                match lexer.next_token().kind {
                    token::Eof => break,
                    token::Whitespace => (),
                    token::Unknown(..) => has_syntax_errors = true,
                    _ => only_whitespace = false,
                }
            }

            if has_syntax_errors {
                Some(CodeBlockInvalid::SyntaxError)
            } else if only_whitespace {
                Some(CodeBlockInvalid::Empty)
            } else {
                None
            }
        };

        if let Some(code_block_invalid) = validation_status {
            let mut diag = if let Some(sp) =
                super::source_span_for_markdown_range(self.cx, &dox, &code_block.range, &item.attrs)
            {
                let warning_message = match code_block_invalid {
                    CodeBlockInvalid::SyntaxError => "could not parse code block as Rust code",
                    CodeBlockInvalid::Empty => "Rust code block is empty",
                };

                let mut diag = self.cx.sess().struct_span_warn(sp, warning_message);

                if code_block.syntax.is_none() && code_block.is_fenced {
                    let sp = sp.from_inner(InnerSpan::new(0, 3));
                    diag.span_suggestion(
                        sp,
                        "mark blocks that do not contain Rust code as text",
                        String::from("```text"),
                        Applicability::MachineApplicable,
                    );
                }

                diag
            } else {
                // We couldn't calculate the span of the markdown block that had the error, so our
                // diagnostics are going to be a bit lacking.
                let mut diag = self.cx.sess().struct_span_warn(
                    super::span_of_attrs(&item.attrs).unwrap_or(item.source.span()),
                    "doc comment contains an invalid Rust code block",
                );

                if code_block.syntax.is_none() && code_block.is_fenced {
                    diag.help("mark blocks that do not contain Rust code as text: ```text");
                }

                diag
            };

            diag.emit();
        }
    }
}

impl<'a, 'tcx> DocFolder for SyntaxChecker<'a, 'tcx> {
    fn fold_item(&mut self, item: clean::Item) -> Option<clean::Item> {
        if let Some(dox) = &item.attrs.collapsed_doc_value() {
            for code_block in markdown::rust_code_blocks(&dox) {
                self.check_rust_syntax(&item, &dox, code_block);
            }
        }

        self.fold_item_recur(item)
    }
}

enum CodeBlockInvalid {
    SyntaxError,
    Empty,
}