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 version = cmd!(sh, "rustup run stable rustfmt --version").read().unwrap_or_default();
134
135 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#[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 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}