]> git.proxmox.com Git - rustc.git/blame - src/tools/rust-analyzer/crates/sourcegen/src/lib.rs
New upstream version 1.68.2+dfsg1
[rustc.git] / src / tools / rust-analyzer / crates / sourcegen / src / lib.rs
CommitLineData
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
11use std::{
12 fmt, fs, mem,
13 path::{Path, PathBuf},
14};
15
16use xshell::{cmd, Shell};
17
18pub 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
26pub 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)]
49pub struct CommentBlock {
50 pub id: String,
51 pub line: usize,
52 pub contents: Vec<String>,
53 is_doc: bool,
54}
55
56impl 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)]
115pub struct Location {
116 pub file: PathBuf,
117 pub line: usize,
118}
119
120impl 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
135fn 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
145pub 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
162pub 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.
170pub 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
193fn normalize_newlines(s: &str) -> String {
194 s.replace("\r\n", "\n")
195}
196
197pub 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}