Skip to content

Commit 4e8cbf3

Browse files
committed
Auto merge of #16708 - Veykril:codegen, r=Veykril
internal: Move ide-assists codegen tests into an xtask codegen command
2 parents 00879b1 + 03b02e6 commit 4e8cbf3

File tree

11 files changed

+273
-29
lines changed

11 files changed

+273
-29
lines changed

.cargo/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ xtask = "run --package xtask --bin xtask --"
33
tq = "test -- -q"
44
qt = "tq"
55
lint = "clippy --all-targets -- --cap-lints warn"
6+
codegen = "run --package xtask --bin xtask -- codegen"
67

78
[target.x86_64-pc-windows-msvc]
89
linker = "rust-lld"

.github/workflows/ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ jobs:
7979
if: matrix.os == 'ubuntu-latest'
8080
run: sed -i '/\[profile.dev]/a opt-level=1' Cargo.toml
8181

82+
- name: Codegen checks (rust-analyzer)
83+
run: cargo codegen --check
84+
8285
- name: Compile (tests)
8386
run: cargo test --no-run --locked ${{ env.USE_SYSROOT_ABI }}
8487

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ide-assists/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ expect-test = "1.4.0"
3232
# local deps
3333
test-utils.workspace = true
3434
test-fixture.workspace = true
35-
sourcegen.workspace = true
3635

3736
[features]
3837
in-rust-tree = []

crates/ide-assists/src/tests.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
mod generated;
2-
#[cfg(not(feature = "in-rust-tree"))]
3-
mod sourcegen;
42

53
use expect_test::expect;
64
use hir::Semantics;

xtask/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ xshell.workspace = true
1414
xflags = "0.3.0"
1515
time = { version = "0.3", default-features = false }
1616
zip = { version = "0.6", default-features = false, features = ["deflate", "time"] }
17+
stdx.workspace = true
1718
# Avoid adding more dependencies to this crate
1819

1920
[lints]
20-
workspace = true
21+
workspace = true

xtask/src/codegen.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use std::{
2+
fmt, fs, mem,
3+
path::{Path, PathBuf},
4+
};
5+
6+
use xshell::{cmd, Shell};
7+
8+
use crate::{flags, project_root};
9+
10+
pub(crate) mod assists_doc_tests;
11+
12+
impl flags::Codegen {
13+
pub(crate) fn run(self, _sh: &Shell) -> anyhow::Result<()> {
14+
match self.codegen_type.unwrap_or_default() {
15+
flags::CodegenType::All => {
16+
assists_doc_tests::generate(self.check);
17+
}
18+
flags::CodegenType::AssistsDocTests => assists_doc_tests::generate(self.check),
19+
}
20+
Ok(())
21+
}
22+
}
23+
24+
fn list_rust_files(dir: &Path) -> Vec<PathBuf> {
25+
let mut res = list_files(dir);
26+
res.retain(|it| {
27+
it.file_name().unwrap_or_default().to_str().unwrap_or_default().ends_with(".rs")
28+
});
29+
res
30+
}
31+
32+
fn list_files(dir: &Path) -> Vec<PathBuf> {
33+
let mut res = Vec::new();
34+
let mut work = vec![dir.to_path_buf()];
35+
while let Some(dir) = work.pop() {
36+
for entry in dir.read_dir().unwrap() {
37+
let entry = entry.unwrap();
38+
let file_type = entry.file_type().unwrap();
39+
let path = entry.path();
40+
let is_hidden =
41+
path.file_name().unwrap_or_default().to_str().unwrap_or_default().starts_with('.');
42+
if !is_hidden {
43+
if file_type.is_dir() {
44+
work.push(path);
45+
} else if file_type.is_file() {
46+
res.push(path);
47+
}
48+
}
49+
}
50+
}
51+
res
52+
}
53+
54+
#[derive(Clone)]
55+
pub(crate) struct CommentBlock {
56+
pub(crate) id: String,
57+
pub(crate) line: usize,
58+
pub(crate) contents: Vec<String>,
59+
is_doc: bool,
60+
}
61+
62+
impl CommentBlock {
63+
fn extract(tag: &str, text: &str) -> Vec<CommentBlock> {
64+
assert!(tag.starts_with(char::is_uppercase));
65+
66+
let tag = format!("{tag}:");
67+
let mut blocks = CommentBlock::extract_untagged(text);
68+
blocks.retain_mut(|block| {
69+
let first = block.contents.remove(0);
70+
let Some(id) = first.strip_prefix(&tag) else {
71+
return false;
72+
};
73+
74+
if block.is_doc {
75+
panic!("Use plain (non-doc) comments with tags like {tag}:\n {first}");
76+
}
77+
78+
block.id = id.trim().to_owned();
79+
true
80+
});
81+
blocks
82+
}
83+
84+
fn extract_untagged(text: &str) -> Vec<CommentBlock> {
85+
let mut res = Vec::new();
86+
87+
let lines = text.lines().map(str::trim_start);
88+
89+
let dummy_block =
90+
CommentBlock { id: String::new(), line: 0, contents: Vec::new(), is_doc: false };
91+
let mut block = dummy_block.clone();
92+
for (line_num, line) in lines.enumerate() {
93+
match line.strip_prefix("//") {
94+
Some(mut contents) => {
95+
if let Some('/' | '!') = contents.chars().next() {
96+
contents = &contents[1..];
97+
block.is_doc = true;
98+
}
99+
if let Some(' ') = contents.chars().next() {
100+
contents = &contents[1..];
101+
}
102+
block.contents.push(contents.to_owned());
103+
}
104+
None => {
105+
if !block.contents.is_empty() {
106+
let block = mem::replace(&mut block, dummy_block.clone());
107+
res.push(block);
108+
}
109+
block.line = line_num + 2;
110+
}
111+
}
112+
}
113+
if !block.contents.is_empty() {
114+
res.push(block);
115+
}
116+
res
117+
}
118+
}
119+
120+
#[derive(Debug)]
121+
pub(crate) struct Location {
122+
pub(crate) file: PathBuf,
123+
pub(crate) line: usize,
124+
}
125+
126+
impl fmt::Display for Location {
127+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128+
let path = self.file.strip_prefix(project_root()).unwrap().display().to_string();
129+
let path = path.replace('\\', "/");
130+
let name = self.file.file_name().unwrap();
131+
write!(
132+
f,
133+
"https://github.com/rust-lang/rust-analyzer/blob/master/{}#L{}[{}]",
134+
path,
135+
self.line,
136+
name.to_str().unwrap()
137+
)
138+
}
139+
}
140+
141+
fn ensure_rustfmt(sh: &Shell) {
142+
let version = cmd!(sh, "rustup run stable rustfmt --version").read().unwrap_or_default();
143+
if !version.contains("stable") {
144+
panic!(
145+
"Failed to run rustfmt from toolchain 'stable'. \
146+
Please run `rustup component add rustfmt --toolchain stable` to install it.",
147+
);
148+
}
149+
}
150+
151+
fn reformat(text: String) -> String {
152+
let sh = Shell::new().unwrap();
153+
ensure_rustfmt(&sh);
154+
let rustfmt_toml = project_root().join("rustfmt.toml");
155+
let mut stdout = cmd!(
156+
sh,
157+
"rustup run stable rustfmt --config-path {rustfmt_toml} --config fn_single_line=true"
158+
)
159+
.stdin(text)
160+
.read()
161+
.unwrap();
162+
if !stdout.ends_with('\n') {
163+
stdout.push('\n');
164+
}
165+
stdout
166+
}
167+
168+
fn add_preamble(generator: &'static str, mut text: String) -> String {
169+
let preamble = format!("//! Generated by `{generator}`, do not edit by hand.\n\n");
170+
text.insert_str(0, &preamble);
171+
text
172+
}
173+
174+
/// Checks that the `file` has the specified `contents`. If that is not the
175+
/// case, updates the file and then fails the test.
176+
#[allow(clippy::print_stderr)]
177+
fn ensure_file_contents(file: &Path, contents: &str, check: bool) {
178+
if let Ok(old_contents) = fs::read_to_string(file) {
179+
if normalize_newlines(&old_contents) == normalize_newlines(contents) {
180+
// File is already up to date.
181+
return;
182+
}
183+
}
184+
185+
let display_path = file.strip_prefix(project_root()).unwrap_or(file);
186+
if check {
187+
panic!(
188+
"{} was not up-to-date{}",
189+
file.display(),
190+
if std::env::var("CI").is_ok() {
191+
"\n NOTE: run `cargo codegen` locally and commit the updated files\n"
192+
} else {
193+
""
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+
}
207+
}
208+
209+
fn normalize_newlines(s: &str) -> String {
210+
s.replace("\r\n", "\n")
211+
}

crates/ide-assists/src/tests/sourcegen.rs renamed to xtask/src/codegen/assists_doc_tests.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
use std::{fmt, fs, path::Path};
44

55
use stdx::format_to_acc;
6-
use test_utils::project_root;
76

8-
#[test]
9-
fn sourcegen_assists_docs() {
7+
use crate::{
8+
codegen::{
9+
add_preamble, ensure_file_contents, list_rust_files, reformat, CommentBlock, Location,
10+
},
11+
project_root,
12+
};
13+
14+
pub(crate) fn generate(check: bool) {
1015
let assists = Assist::collect();
1116

1217
{
@@ -40,10 +45,11 @@ r#####"
4045
buf.push_str(&test)
4146
}
4247
}
43-
let buf = sourcegen::add_preamble("sourcegen_assists_docs", sourcegen::reformat(buf));
44-
sourcegen::ensure_file_contents(
48+
let buf = add_preamble("sourcegen_assists_docs", reformat(buf));
49+
ensure_file_contents(
4550
&project_root().join("crates/ide-assists/src/tests/generated.rs"),
4651
&buf,
52+
check,
4753
);
4854
}
4955

@@ -52,7 +58,7 @@ r#####"
5258
// git repo. Instead, `cargo xtask release` runs this test before making
5359
// a release.
5460

55-
let contents = sourcegen::add_preamble(
61+
let contents = add_preamble(
5662
"sourcegen_assists_docs",
5763
assists.into_iter().map(|it| it.to_string()).collect::<Vec<_>>().join("\n\n"),
5864
);
@@ -71,7 +77,7 @@ struct Section {
7177
#[derive(Debug)]
7278
struct Assist {
7379
id: String,
74-
location: sourcegen::Location,
80+
location: Location,
7581
sections: Vec<Section>,
7682
}
7783

@@ -80,15 +86,15 @@ impl Assist {
8086
let handlers_dir = project_root().join("crates/ide-assists/src/handlers");
8187

8288
let mut res = Vec::new();
83-
for path in sourcegen::list_rust_files(&handlers_dir) {
89+
for path in list_rust_files(&handlers_dir) {
8490
collect_file(&mut res, path.as_path());
8591
}
8692
res.sort_by(|lhs, rhs| lhs.id.cmp(&rhs.id));
8793
return res;
8894

8995
fn collect_file(acc: &mut Vec<Assist>, path: &Path) {
9096
let text = fs::read_to_string(path).unwrap();
91-
let comment_blocks = sourcegen::CommentBlock::extract("Assist", &text);
97+
let comment_blocks = CommentBlock::extract("Assist", &text);
9298

9399
for block in comment_blocks {
94100
let id = block.id;
@@ -97,7 +103,7 @@ impl Assist {
97103
"invalid assist id: {id:?}"
98104
);
99105
let mut lines = block.contents.iter().peekable();
100-
let location = sourcegen::Location { file: path.to_path_buf(), line: block.line };
106+
let location = Location { file: path.to_path_buf(), line: block.line };
101107
let mut assist = Assist { id, location, sections: Vec::new() };
102108

103109
while lines.peek().is_some() {

xtask/src/flags.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ xflags::xflags! {
5252
cmd bb {
5353
required suffix: String
5454
}
55+
56+
cmd codegen {
57+
optional codegen_type: CodegenType
58+
optional --check
59+
}
5560
}
5661
}
5762

@@ -73,8 +78,32 @@ pub enum XtaskCmd {
7378
PublishReleaseNotes(PublishReleaseNotes),
7479
Metrics(Metrics),
7580
Bb(Bb),
81+
Codegen(Codegen),
82+
}
83+
84+
#[derive(Debug)]
85+
pub struct Codegen {
86+
pub check: bool,
87+
pub codegen_type: Option<CodegenType>,
88+
}
89+
90+
#[derive(Debug, Default)]
91+
pub enum CodegenType {
92+
#[default]
93+
All,
94+
AssistsDocTests,
7695
}
7796

97+
impl FromStr for CodegenType {
98+
type Err = String;
99+
fn from_str(s: &str) -> Result<Self, Self::Err> {
100+
match s {
101+
"all" => Ok(Self::All),
102+
"assists-doc-tests" => Ok(Self::AssistsDocTests),
103+
_ => Err("Invalid option".to_owned()),
104+
}
105+
}
106+
}
78107
#[derive(Debug)]
79108
pub struct Install {
80109
pub client: bool,

0 commit comments

Comments
 (0)