ra_ap_sourcegen/
lib.rs

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)]
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
60        let tag = format!("{tag}:");
61        let mut blocks = CommentBlock::extract_untagged(text);
62        blocks.retain_mut(|block| {
63            let first = block.contents.remove(0);
64            let Some(id) = first.strip_prefix(&tag) else {
65                return false;
66            };
67
68            if block.is_doc {
69                panic!("Use plain (non-doc) comments with tags like {tag}:\n    {first}");
70            }
71
72            block.id = id.trim().to_string();
73            true
74        });
75        blocks
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 {
122        let path = self.file.strip_prefix(project_root()).unwrap().display().to_string();
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) {
136    let version = cmd!(sh, "rustup run stable rustfmt --version").read().unwrap_or_default();
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();
147    ensure_rustfmt(&sh);
148    let rustfmt_toml = project_root().join("rustfmt.toml");
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();
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 {
163    let preamble = format!("//! Generated by `{generator}`, do not edit by hand.\n\n");
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
178    let display_path = file.strip_prefix(project_root()).unwrap_or(file);
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}