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 version = cmd!(sh, "rustup run stable rustfmt --version").read().unwrap_or_default();
134
135    // First try explicitly requesting the stable channel via rustup in case nightly is being used by default,
136    // then plain rustfmt in case rustup isn't being used to manage the compiler (e.g. when using Nix).
137    let mut stdout = if !version.contains("stable") {
138        let version = cmd!(sh, "rustfmt --version").read().unwrap_or_default();
139        if !version.contains("stable") {
140            panic!(
141                "Failed to run rustfmt from toolchain 'stable'. \
142                 Please run `rustup component add rustfmt --toolchain stable` to install it.",
143            );
144        } else {
145            cmd!(sh, "rustfmt --config-path {rustfmt_toml} --config fn_single_line=true")
146                .stdin(text)
147                .read()
148                .unwrap()
149        }
150    } else {
151        cmd!(
152            sh,
153            "rustup run stable rustfmt --config-path {rustfmt_toml} --config fn_single_line=true"
154        )
155        .stdin(text)
156        .read()
157        .unwrap()
158    };
159    if !stdout.ends_with('\n') {
160        stdout.push('\n');
161    }
162    stdout
163}
164
165fn add_preamble(cg: CodegenType, mut text: String) -> String {
166    let preamble = format!("//! Generated by `cargo xtask codegen {cg}`, do not edit by hand.\n\n");
167    text.insert_str(0, &preamble);
168    text
169}
170
171/// Checks that the `file` has the specified `contents`. If that is not the
172/// case, updates the file and then fails the test.
173#[allow(clippy::print_stderr)]
174fn ensure_file_contents(cg: CodegenType, file: &Path, contents: &str, check: bool) -> bool {
175    let contents = normalize_newlines(contents);
176    if let Ok(old_contents) = fs::read_to_string(file)
177        && normalize_newlines(&old_contents) == contents
178    {
179        // File is already up to date.
180        return false;
181    }
182
183    let display_path = file.strip_prefix(project_root()).unwrap_or(file);
184    if check {
185        panic!(
186            "{} was not up-to-date{}",
187            file.display(),
188            if std::env::var("CI").is_ok() {
189                format!(
190                    "\n    NOTE: run `cargo xtask codegen {cg}` locally and commit the updated files\n"
191                )
192            } else {
193                "".to_owned()
194            }
195        );
196    } else {
197        eprintln!(
198            "\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n",
199            display_path.display()
200        );
201
202        if let Some(parent) = file.parent() {
203            let _ = fs::create_dir_all(parent);
204        }
205        fs::write(file, contents).unwrap();
206        true
207    }
208}
209
210fn normalize_newlines(s: &str) -> String {
211    s.replace("\r\n", "\n")
212}