]>
Commit | Line | Data |
---|---|---|
064997fb FG |
1 | //! rust-analyzer relies heavily on source code generation. |
2 | //! | |
3 | //! Things like feature documentation or assist tests are implemented by | |
4 | //! processing rust-analyzer's own source code and generating the appropriate | |
5 | //! output. See `sourcegen_` tests in various crates. | |
6 | //! | |
7 | //! This crate contains utilities to make this kind of source-gen easy. | |
8 | ||
9 | #![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)] | |
10 | ||
11 | use std::{ | |
12 | fmt, fs, mem, | |
13 | path::{Path, PathBuf}, | |
14 | }; | |
15 | ||
16 | use xshell::{cmd, Shell}; | |
17 | ||
18 | pub fn list_rust_files(dir: &Path) -> Vec<PathBuf> { | |
19 | let mut res = list_files(dir); | |
20 | res.retain(|it| { | |
21 | it.file_name().unwrap_or_default().to_str().unwrap_or_default().ends_with(".rs") | |
22 | }); | |
23 | res | |
24 | } | |
25 | ||
26 | pub fn list_files(dir: &Path) -> Vec<PathBuf> { | |
27 | let mut res = Vec::new(); | |
28 | let mut work = vec![dir.to_path_buf()]; | |
29 | while let Some(dir) = work.pop() { | |
30 | for entry in dir.read_dir().unwrap() { | |
31 | let entry = entry.unwrap(); | |
32 | let file_type = entry.file_type().unwrap(); | |
33 | let path = entry.path(); | |
34 | let is_hidden = | |
35 | path.file_name().unwrap_or_default().to_str().unwrap_or_default().starts_with('.'); | |
36 | if !is_hidden { | |
37 | if file_type.is_dir() { | |
38 | work.push(path); | |
39 | } else if file_type.is_file() { | |
40 | res.push(path); | |
41 | } | |
42 | } | |
43 | } | |
44 | } | |
45 | res | |
46 | } | |
47 | ||
48 | #[derive(Clone)] | |
49 | pub struct CommentBlock { | |
50 | pub id: String, | |
51 | pub line: usize, | |
52 | pub contents: Vec<String>, | |
53 | is_doc: bool, | |
54 | } | |
55 | ||
56 | impl CommentBlock { | |
57 | pub fn extract(tag: &str, text: &str) -> Vec<CommentBlock> { | |
58 | assert!(tag.starts_with(char::is_uppercase)); | |
59 | ||
6522a427 | 60 | let tag = format!("{tag}:"); |
064997fb FG |
61 | // Would be nice if we had `.retain_mut` here! |
62 | CommentBlock::extract_untagged(text) | |
63 | .into_iter() | |
64 | .filter_map(|mut block| { | |
65 | let first = block.contents.remove(0); | |
66 | first.strip_prefix(&tag).map(|id| { | |
67 | if block.is_doc { | |
6522a427 | 68 | panic!("Use plain (non-doc) comments with tags like {tag}:\n {first}"); |
064997fb FG |
69 | } |
70 | ||
71 | block.id = id.trim().to_string(); | |
72 | block | |
73 | }) | |
74 | }) | |
75 | .collect() | |
76 | } | |
77 | ||
78 | pub fn extract_untagged(text: &str) -> Vec<CommentBlock> { | |
79 | let mut res = Vec::new(); | |
80 | ||
81 | let lines = text.lines().map(str::trim_start); | |
82 | ||
83 | let dummy_block = | |
84 | CommentBlock { id: String::new(), line: 0, contents: Vec::new(), is_doc: false }; | |
85 | let mut block = dummy_block.clone(); | |
86 | for (line_num, line) in lines.enumerate() { | |
87 | match line.strip_prefix("//") { | |
88 | Some(mut contents) => { | |
89 | if let Some('/' | '!') = contents.chars().next() { | |
90 | contents = &contents[1..]; | |
91 | block.is_doc = true; | |
92 | } | |
93 | if let Some(' ') = contents.chars().next() { | |
94 | contents = &contents[1..]; | |
95 | } | |
96 | block.contents.push(contents.to_string()); | |
97 | } | |
98 | None => { | |
99 | if !block.contents.is_empty() { | |
100 | let block = mem::replace(&mut block, dummy_block.clone()); | |
101 | res.push(block); | |
102 | } | |
103 | block.line = line_num + 2; | |
104 | } | |
105 | } | |
106 | } | |
107 | if !block.contents.is_empty() { | |
108 | res.push(block); | |
109 | } | |
110 | res | |
111 | } | |
112 | } | |
113 | ||
114 | #[derive(Debug)] | |
115 | pub struct Location { | |
116 | pub file: PathBuf, | |
117 | pub line: usize, | |
118 | } | |
119 | ||
120 | impl fmt::Display for Location { | |
121 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
6522a427 | 122 | let path = self.file.strip_prefix(project_root()).unwrap().display().to_string(); |
064997fb FG |
123 | let path = path.replace('\\', "/"); |
124 | let name = self.file.file_name().unwrap(); | |
125 | write!( | |
126 | f, | |
127 | "https://github.com/rust-lang/rust-analyzer/blob/master/{}#L{}[{}]", | |
128 | path, | |
129 | self.line, | |
130 | name.to_str().unwrap() | |
131 | ) | |
132 | } | |
133 | } | |
134 | ||
135 | fn ensure_rustfmt(sh: &Shell) { | |
f2b60f7d | 136 | let version = cmd!(sh, "rustup run stable rustfmt --version").read().unwrap_or_default(); |
064997fb FG |
137 | if !version.contains("stable") { |
138 | panic!( | |
139 | "Failed to run rustfmt from toolchain 'stable'. \ | |
140 | Please run `rustup component add rustfmt --toolchain stable` to install it.", | |
141 | ); | |
142 | } | |
143 | } | |
144 | ||
145 | pub fn reformat(text: String) -> String { | |
146 | let sh = Shell::new().unwrap(); | |
064997fb FG |
147 | ensure_rustfmt(&sh); |
148 | let rustfmt_toml = project_root().join("rustfmt.toml"); | |
f2b60f7d FG |
149 | let mut stdout = cmd!( |
150 | sh, | |
151 | "rustup run stable rustfmt --config-path {rustfmt_toml} --config fn_single_line=true" | |
152 | ) | |
153 | .stdin(text) | |
154 | .read() | |
155 | .unwrap(); | |
064997fb FG |
156 | if !stdout.ends_with('\n') { |
157 | stdout.push('\n'); | |
158 | } | |
159 | stdout | |
160 | } | |
161 | ||
162 | pub fn add_preamble(generator: &'static str, mut text: String) -> String { | |
6522a427 | 163 | let preamble = format!("//! Generated by `{generator}`, do not edit by hand.\n\n"); |
064997fb FG |
164 | text.insert_str(0, &preamble); |
165 | text | |
166 | } | |
167 | ||
168 | /// Checks that the `file` has the specified `contents`. If that is not the | |
169 | /// case, updates the file and then fails the test. | |
170 | pub fn ensure_file_contents(file: &Path, contents: &str) { | |
171 | if let Ok(old_contents) = fs::read_to_string(file) { | |
172 | if normalize_newlines(&old_contents) == normalize_newlines(contents) { | |
173 | // File is already up to date. | |
174 | return; | |
175 | } | |
176 | } | |
177 | ||
6522a427 | 178 | let display_path = file.strip_prefix(project_root()).unwrap_or(file); |
064997fb FG |
179 | eprintln!( |
180 | "\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n", | |
181 | display_path.display() | |
182 | ); | |
183 | if std::env::var("CI").is_ok() { | |
184 | eprintln!(" NOTE: run `cargo test` locally and commit the updated files\n"); | |
185 | } | |
186 | if let Some(parent) = file.parent() { | |
187 | let _ = fs::create_dir_all(parent); | |
188 | } | |
189 | fs::write(file, contents).unwrap(); | |
190 | panic!("some file was not up to date and has been updated, simply re-run the tests"); | |
191 | } | |
192 | ||
193 | fn normalize_newlines(s: &str) -> String { | |
194 | s.replace("\r\n", "\n") | |
195 | } | |
196 | ||
197 | pub fn project_root() -> PathBuf { | |
198 | let dir = env!("CARGO_MANIFEST_DIR"); | |
199 | let res = PathBuf::from(dir).parent().unwrap().parent().unwrap().to_owned(); | |
200 | assert!(res.join("triagebot.toml").exists()); | |
201 | res | |
202 | } |