Skip to content

Commit 9090abb

Browse files
committed
Flycheck
1 parent d8466b7 commit 9090abb

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

tools/rust_analyzer/BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ package(default_visibility = ["//visibility:public"])
77
rust_binary(
88
name = "discover_bazel_rust_project",
99
srcs = ["@rules_rust//tools/rust_analyzer:bin/discover_rust_project.rs"],
10+
data = [":flycheck"],
1011
edition = "2018",
1112
rustc_env = {
1213
"ASPECT_REPOSITORY": aspect_repository(),
14+
"FLYCHECK_RLOCATIONPATH": "$(rlocationpath :flycheck)",
1315
},
1416
deps = [
1517
":gen_rust_project_lib",
@@ -19,15 +21,29 @@ rust_binary(
1921
"@rrra//:env_logger",
2022
"@rrra//:log",
2123
"@rrra//:serde_json",
24+
"@rules_rust//rust/runfiles",
25+
],
26+
)
27+
28+
rust_binary(
29+
name = "flycheck",
30+
srcs = ["flycheck.rs"],
31+
edition = "2021",
32+
deps = [
33+
"@rrra//:clap",
34+
"@rrra//:serde",
35+
"@rrra//:serde_json",
2236
],
2337
)
2438

2539
rust_binary(
2640
name = "gen_rust_project",
2741
srcs = ["@rules_rust//tools/rust_analyzer:bin/gen_rust_project.rs"],
42+
data = [":flycheck"],
2843
edition = "2018",
2944
rustc_env = {
3045
"ASPECT_REPOSITORY": aspect_repository(),
46+
"FLYCHECK_RLOCATIONPATH": "$(rlocationpath :flycheck)",
3147
},
3248
deps = [
3349
":gen_rust_project_lib",
@@ -37,6 +53,7 @@ rust_binary(
3753
"@rrra//:env_logger",
3854
"@rrra//:log",
3955
"@rrra//:serde_json",
56+
"@rules_rust//rust/runfiles",
4057
],
4158
)
4259

tools/rust_analyzer/flycheck.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//! Reads every `rust_analyzer_check_command` file the rules_rust analyzer
2+
//! aspect produced for the saved file's Bazel package, then exec's each one to
3+
//! typecheck the corresponding crate via rustc directly. No bazel hop per save.
4+
5+
use std::collections::BTreeMap;
6+
use std::fs;
7+
use std::io;
8+
use std::path::{Path, PathBuf};
9+
use std::process::{Command, ExitCode, Stdio};
10+
11+
use clap::Parser;
12+
use serde::Deserialize;
13+
14+
const WORKSPACE_MARKERS: &[&str] = &["MODULE.bazel", "REPO.bazel", "WORKSPACE.bazel", "WORKSPACE"];
15+
const BUILD_FILES: &[&str] = &["BUILD.bazel", "BUILD"];
16+
const CHECK_COMMAND_SUFFIX: &str = ".rust_analyzer_check_command.json";
17+
18+
#[derive(Parser)]
19+
#[command(version)]
20+
struct CommandLine {
21+
/// Absolute path of the file that was just saved.
22+
saved_file: PathBuf,
23+
}
24+
25+
#[derive(Debug, Deserialize)]
26+
struct CheckCommand {
27+
argv: Vec<String>,
28+
env: BTreeMap<String, String>,
29+
}
30+
31+
fn main() -> ExitCode {
32+
let command_line = CommandLine::parse();
33+
let Ok(saved_file) = command_line.saved_file.canonicalize() else {
34+
return ExitCode::SUCCESS;
35+
};
36+
let Some(workspace_root) = find_workspace_root(&saved_file) else {
37+
return ExitCode::SUCCESS;
38+
};
39+
let Ok(file_relative) = saved_file.strip_prefix(&workspace_root) else {
40+
return ExitCode::SUCCESS;
41+
};
42+
let Some(package_dir) = find_package_dir(&workspace_root, file_relative) else {
43+
return ExitCode::SUCCESS;
44+
};
45+
let Some(execroot) = resolve_execroot(&workspace_root) else {
46+
return ExitCode::SUCCESS;
47+
};
48+
49+
let bin_dir = workspace_root.join("bazel-bin").join(&package_dir);
50+
for check_command_file in collect_check_command_files(&bin_dir) {
51+
run_check_command(&execroot, &check_command_file);
52+
}
53+
ExitCode::SUCCESS
54+
}
55+
56+
fn find_workspace_root(saved_file: &Path) -> Option<PathBuf> {
57+
let mut dir = saved_file.parent()?;
58+
loop {
59+
if WORKSPACE_MARKERS
60+
.iter()
61+
.any(|marker| dir.join(marker).is_file())
62+
{
63+
return Some(dir.to_path_buf());
64+
}
65+
dir = dir.parent()?;
66+
}
67+
}
68+
69+
fn find_package_dir(workspace_root: &Path, file_relative: &Path) -> Option<PathBuf> {
70+
let mut dir = file_relative.parent()?.to_path_buf();
71+
loop {
72+
let absolute = workspace_root.join(&dir);
73+
if BUILD_FILES
74+
.iter()
75+
.any(|name| absolute.join(name).is_file())
76+
{
77+
return Some(dir);
78+
}
79+
if !dir.pop() {
80+
return None;
81+
}
82+
}
83+
}
84+
85+
/// rustc resolves transitive crate lookups relative to its cwd; running from
86+
/// the repo root (where `bazel-out` is a symlink) breaks lookups for some
87+
/// crates. Bazel runs the same actions from `execroot`, which is the parent of
88+
/// the real `bazel-out` directory.
89+
fn resolve_execroot(workspace_root: &Path) -> Option<PathBuf> {
90+
let bazel_out = workspace_root.join("bazel-out").canonicalize().ok()?;
91+
bazel_out.parent().map(Path::to_path_buf)
92+
}
93+
94+
fn collect_check_command_files(bin_dir: &Path) -> Vec<PathBuf> {
95+
let Ok(entries) = fs::read_dir(bin_dir) else {
96+
return Vec::new();
97+
};
98+
entries
99+
.flatten()
100+
.map(|entry| entry.path())
101+
.filter(|path| {
102+
path.file_name()
103+
.and_then(|n| n.to_str())
104+
.is_some_and(|name| name.ends_with(CHECK_COMMAND_SUFFIX))
105+
})
106+
.collect()
107+
}
108+
109+
fn run_check_command(execroot: &Path, path: &Path) {
110+
let Ok(contents) = fs::read_to_string(path) else {
111+
return;
112+
};
113+
let Ok(command) = serde_json::from_str::<CheckCommand>(&contents) else {
114+
return;
115+
};
116+
let Some((program, args)) = command.argv.split_first() else {
117+
return;
118+
};
119+
// rustc writes JSON diagnostics to stderr; rust-analyzer reads them from
120+
// our stdout. Hand the child a clone of our stdout fd to use as its
121+
// stderr, so its diagnostics land where the editor is listening.
122+
let stderr_for_child = clone_stdout_as_stderr_target().unwrap_or_else(Stdio::inherit);
123+
let _ = Command::new(program)
124+
.args(args)
125+
.envs(&command.env)
126+
.current_dir(execroot)
127+
.stderr(stderr_for_child)
128+
.status();
129+
}
130+
131+
#[cfg(unix)]
132+
fn clone_stdout_as_stderr_target() -> Option<Stdio> {
133+
use std::os::fd::AsFd;
134+
io::stdout().as_fd().try_clone_to_owned().ok().map(Stdio::from)
135+
}
136+
137+
#[cfg(not(unix))]
138+
fn clone_stdout_as_stderr_target() -> Option<Stdio> {
139+
None
140+
}

0 commit comments

Comments
 (0)