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 }
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 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#[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 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}