Skip to main content

xtask/
codegen.rs

1use std::{
2    fmt, fs, mem,
3    path::{Path, PathBuf},
4};
5
6use xshell::{Shell, cmd};
7
8use crate::{
9    flags::{self, CodegenType},
10    project_root,
11};
12
13pub(crate) mod assists_doc_tests;
14pub(crate) mod diagnostics_docs;
15pub(crate) mod feature_docs;
16mod grammar;
17mod lints;
18mod parser_inline_tests;
19
20impl flags::Codegen {
21    pub(crate) fn run(self, _sh: &Shell) -> anyhow::Result<()> {
22        match self.codegen_type.unwrap_or_default() {
23            flags::CodegenType::All => {
24                grammar::generate(self.check);
25                assists_doc_tests::generate(self.check);
26                parser_inline_tests::generate(self.check);
27                feature_docs::generate(self.check);
28                diagnostics_docs::generate(self.check);
29                // lints::generate(self.check) Updating clones the rust repo, so don't run it unless
30                // explicitly asked for
31            }
32            flags::CodegenType::Grammar => grammar::generate(self.check),
33            flags::CodegenType::AssistsDocTests => assists_doc_tests::generate(self.check),
34            flags::CodegenType::DiagnosticsDocs => diagnostics_docs::generate(self.check),
35            flags::CodegenType::LintDefinitions => lints::generate(self.check),
36            flags::CodegenType::ParserTests => parser_inline_tests::generate(self.check),
37            flags::CodegenType::FeatureDocs => feature_docs::generate(self.check),
38        }
39        Ok(())
40    }
41}
42
43#[derive(Clone)]
44pub(crate) struct CommentBlock {
45    pub(crate) id: String,
46    pub(crate) line: usize,
47    pub(crate) contents: Vec<String>,
48    is_doc: bool,
49}
50
51impl CommentBlock {
52    fn extract(tag: &str, text: &str) -> Vec<CommentBlock> {
53        assert!(tag.starts_with(char::is_uppercase));
54
55        let tag = format!("{tag}:");
56        let mut blocks = CommentBlock::extract_untagged(text);
57        blocks.retain_mut(|block| {
58            let first = block.contents.remove(0);
59            let Some(id) = first.strip_prefix(&tag) else {
60                return false;
61            };
62
63            if block.is_doc {
64                panic!("Use plain (non-doc) comments with tags like {tag}:\n    {first}");
65            }
66
67            id.trim().clone_into(&mut block.id);
68            true
69        });
70        blocks
71    }
72
73    fn extract_untagged(text: &str) -> Vec<CommentBlock> {
74        let mut res = Vec::new();
75
76        let lines = text.lines().map(str::trim_start);
77
78        let dummy_block =
79            CommentBlock { id: String::new(), line: 0, contents: Vec::new(), is_doc: false };
80        let mut block = dummy_block.clone();
81        for (line_num, line) in lines.enumerate() {
82            match line.strip_prefix("//") {
83                Some(mut contents) if !contents.starts_with('/') => {
84                    if let Some('/' | '!') = contents.chars().next() {
85                        contents = &contents[1..];
86                        block.is_doc = true;
87                    }
88                    if let Some(' ') = contents.chars().next() {
89                        contents = &contents[1..];
90                    }
91                    block.contents.push(contents.to_owned());
92                }
93                _ => {
94                    if !block.contents.is_empty() {
95                        let block = mem::replace(&mut block, dummy_block.clone());
96                        res.push(block);
97                    }
98                    block.line = line_num + 2;
99                }
100            }
101        }
102        if !block.contents.is_empty() {
103            res.push(block);
104        }
105        res
106    }
107}
108
109#[derive(Debug)]
110pub(crate) struct Location {
111    pub(crate) file: PathBuf,
112    pub(crate) line: usize,
113}
114
115impl fmt::Display for Location {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        let path = self.file.strip_prefix(project_root()).unwrap().display().to_string();
118        let path = path.replace('\\', "/");
119        let name = self.file.file_name().unwrap();
120        write!(
121            f,
122            " [{}](https://github.com/rust-lang/rust-analyzer/blob/master/{}#L{}) ",
123            name.to_str().unwrap(),
124            path,
125            self.line
126        )
127    }
128}
129
130fn reformat(text: String) -> String {
131    let sh = Shell::new().unwrap();
132    let rustfmt_toml = project_root().join("rustfmt.toml");
133    let toolchain = &std::env::var("RUSTFMT_TOOLCHAIN").unwrap_or("stable".to_owned());
134    let version = cmd!(sh, "rustup run {toolchain} rustfmt --version").read().unwrap_or_default();
135
136    // First try explicitly requesting the stable channel via rustup in case nightly is being used by default,
137    // then plain rustfmt in case rustup isn't being used to manage the compiler (e.g. when using Nix).
138    let mut stdout = if !version.contains(toolchain) {
139        let version = cmd!(sh, "rustfmt --version").read().unwrap_or_default();
140        if !version.contains(toolchain) {
141            panic!(
142                "Failed to run rustfmt from toolchain '{toolchain}'. \
143                 Please run `rustup component add rustfmt --toolchain {toolchain}` to install it.",
144            );
145        } else {
146            cmd!(sh, "rustfmt --config-path {rustfmt_toml} --config fn_single_line=true")
147                .stdin(text)
148                .read()
149                .unwrap()
150        }
151    } else {
152        cmd!(
153            sh,
154            "rustup run {toolchain} rustfmt --config-path {rustfmt_toml} --config fn_single_line=true"
155        )
156        .stdin(text)
157        .read()
158        .unwrap()
159    };
160    if !stdout.ends_with('\n') {
161        stdout.push('\n');
162    }
163    stdout
164}
165
166fn add_preamble(cg: CodegenType, mut text: String) -> String {
167    let preamble = format!("//! Generated by `cargo xtask codegen {cg}`, do not edit by hand.\n\n");
168    text.insert_str(0, &preamble);
169    text
170}
171
172/// Checks that the `file` has the specified `contents`. If that is not the
173/// case, updates the file and then fails the test.
174#[allow(clippy::print_stderr)]
175fn ensure_file_contents(cg: CodegenType, file: &Path, contents: &str, check: bool) -> bool {
176    let contents = normalize_newlines(contents);
177    if let Ok(old_contents) = fs::read_to_string(file)
178        && normalize_newlines(&old_contents) == contents
179    {
180        // File is already up to date.
181        return false;
182    }
183
184    let display_path = file.strip_prefix(project_root()).unwrap_or(file);
185    if check {
186        panic!(
187            "{} was not up-to-date{}",
188            file.display(),
189            if std::env::var("CI").is_ok() {
190                format!(
191                    "\n    NOTE: run `cargo xtask codegen {cg}` locally and commit the updated files\n"
192                )
193            } else {
194                "".to_owned()
195            }
196        );
197    } else {
198        eprintln!(
199            "\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n",
200            display_path.display()
201        );
202
203        if let Some(parent) = file.parent() {
204            let _ = fs::create_dir_all(parent);
205        }
206        fs::write(file, contents).unwrap();
207        true
208    }
209}
210
211fn normalize_newlines(s: &str) -> String {
212    s.replace("\r\n", "\n")
213}