xtask/codegen/
assists_doc_tests.rs1use std::{fmt, fs, path::Path};
4
5use stdx::format_to_acc;
6
7use crate::{
8 codegen::{CommentBlock, Location, add_preamble, ensure_file_contents, reformat},
9 project_root,
10 util::list_rust_files,
11};
12
13pub(crate) fn generate(check: bool) {
14 let assists = Assist::collect();
15
16 {
17 let mut buf = "
20use super::check_doc_test;
21"
22 .to_owned();
23 for assist in assists.iter() {
24 for (idx, section) in assist.sections.iter().enumerate() {
25 let test_id =
26 if idx == 0 { assist.id.clone() } else { format!("{}_{idx}", &assist.id) };
27 let test = format!(
28 r######"
29#[test]
30fn doctest_{}() {{
31 check_doc_test(
32 "{}",
33r#####"
34{}"#####, r#####"
35{}"#####)
36}}
37"######,
38 &test_id,
39 &assist.id,
40 reveal_hash_comments(§ion.before),
41 reveal_hash_comments(§ion.after)
42 );
43
44 buf.push_str(&test)
45 }
46 }
47 let buf = add_preamble(crate::flags::CodegenType::AssistsDocTests, reformat(buf));
48 ensure_file_contents(
49 crate::flags::CodegenType::AssistsDocTests,
50 &project_root().join("crates/ide-assists/src/tests/generated.rs"),
51 &buf,
52 check,
53 );
54 }
55
56 if check {
58 return;
59 }
60
61 {
62 let contents = add_preamble(
67 crate::flags::CodegenType::AssistsDocTests,
68 assists.into_iter().map(|it| it.to_string()).collect::<Vec<_>>().join("\n\n"),
69 );
70 let dst = project_root().join("docs/book/src/assists_generated.md");
71 fs::write(dst, contents).unwrap();
72 }
73}
74
75#[derive(Debug)]
76struct Section {
77 doc: String,
78 before: String,
79 after: String,
80}
81
82#[derive(Debug)]
83struct Assist {
84 id: String,
85 location: Location,
86 sections: Vec<Section>,
87}
88
89impl Assist {
90 fn collect() -> Vec<Assist> {
91 let handlers_dir = project_root().join("crates/ide-assists/src/handlers");
92
93 let mut res = Vec::new();
94 for path in list_rust_files(&handlers_dir) {
95 collect_file(&mut res, path.as_path());
96 }
97 res.sort_by(|lhs, rhs| lhs.id.cmp(&rhs.id));
98 return res;
99
100 fn collect_file(acc: &mut Vec<Assist>, path: &Path) {
101 let text = fs::read_to_string(path).unwrap();
102 let comment_blocks = CommentBlock::extract("Assist", &text);
103
104 for block in comment_blocks {
105 let id = block.id;
106 assert!(
107 id.chars().all(|it| it.is_ascii_lowercase() || it == '_'),
108 "invalid assist id: {id:?}"
109 );
110 let mut lines = block.contents.iter().peekable();
111 let location = Location { file: path.to_path_buf(), line: block.line };
112 let mut assist = Assist { id, location, sections: Vec::new() };
113
114 while lines.peek().is_some() {
115 let doc = take_until(lines.by_ref(), "```").trim().to_owned();
116 assert!(
117 (doc.chars().next().unwrap().is_ascii_uppercase() && doc.ends_with('.'))
118 || !assist.sections.is_empty(),
119 "\n\n{}: assist docs should be proper sentences, with capitalization and a full stop at the end.\n\n{}\n\n",
120 &assist.id,
121 doc,
122 );
123
124 let before = take_until(lines.by_ref(), "```");
125
126 assert_eq!(lines.next().unwrap().as_str(), "->");
127 assert_eq!(lines.next().unwrap().as_str(), "```");
128 let after = take_until(lines.by_ref(), "```");
129
130 assist.sections.push(Section { doc, before, after });
131 }
132
133 acc.push(assist)
134 }
135 }
136
137 fn take_until<'a>(lines: impl Iterator<Item = &'a String>, marker: &str) -> String {
138 let mut buf = Vec::new();
139 for line in lines {
140 if line == marker {
141 break;
142 }
143 buf.push(line.clone());
144 }
145 buf.join("\n")
146 }
147 }
148}
149
150impl fmt::Display for Assist {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 let _ = writeln!(
153 f,
154 "### `{}`
155**Source:** {}",
156 self.id, self.location,
157 );
158
159 for section in &self.sections {
160 let before = section.before.replace("$0", "┃"); let after = section.after.replace("$0", "┃");
162 let _ = writeln!(
163 f,
164 "
165{}
166
167#### Before
168```rust
169{}```
170
171#### After
172```rust
173{}```",
174 section.doc,
175 hide_hash_comments(&before),
176 hide_hash_comments(&after)
177 );
178 }
179
180 Ok(())
181 }
182}
183
184fn hide_hash_comments(text: &str) -> String {
185 text.split('\n') .filter(|&it| !(it.starts_with("# ") || it == "#"))
187 .fold(String::new(), |mut acc, it| format_to_acc!(acc, "{it}\n"))
188}
189
190fn reveal_hash_comments(text: &str) -> String {
191 text.split('\n') .map(|it| {
193 if let Some(stripped) = it.strip_prefix("# ") {
194 stripped
195 } else if it == "#" {
196 ""
197 } else {
198 it
199 }
200 })
201 .fold(String::new(), |mut acc, it| format_to_acc!(acc, "{it}\n"))
202}
203
204#[test]
205fn test() {
206 generate(true);
207}