aboutsummaryrefslogtreecommitdiff
path: root/crates/flycheck
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-06-25 08:15:30 +0100
committerGitHub <[email protected]>2020-06-25 08:15:30 +0100
commitedf8f419b532b93102222b9deb2c8bb5080911d9 (patch)
tree824aec6d18f409cc46d28d36161dd06cae53cdaf /crates/flycheck
parent44d525d4e02a8a4f2e21cde3377c4c0bd9269b41 (diff)
parent0ec5d4f55c6a5ac3fadcd48ae17b70379aba17fa (diff)
Merge #5049
5049: Rename ra_flycheck -> flycheck r=matklad a=matklad bors r+ 🤖 Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/flycheck')
-rw-r--r--crates/flycheck/Cargo.toml16
-rw-r--r--crates/flycheck/src/lib.rs342
2 files changed, 358 insertions, 0 deletions
diff --git a/crates/flycheck/Cargo.toml b/crates/flycheck/Cargo.toml
new file mode 100644
index 000000000..dc26b8ce7
--- /dev/null
+++ b/crates/flycheck/Cargo.toml
@@ -0,0 +1,16 @@
1[package]
2edition = "2018"
3name = "flycheck"
4version = "0.1.0"
5authors = ["rust-analyzer developers"]
6
7[lib]
8doctest = false
9
10[dependencies]
11crossbeam-channel = "0.4.0"
12log = "0.4.8"
13cargo_metadata = "0.10.0"
14serde_json = "1.0.48"
15jod-thread = "0.1.1"
16ra_toolchain = { path = "../ra_toolchain" }
diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs
new file mode 100644
index 000000000..af75adbe2
--- /dev/null
+++ b/crates/flycheck/src/lib.rs
@@ -0,0 +1,342 @@
1//! cargo_check provides the functionality needed to run `cargo check` or
2//! another compatible command (f.x. clippy) in a background thread and provide
3//! LSP diagnostics based on the output of the command.
4
5use std::{
6 fmt,
7 io::{self, BufReader},
8 path::PathBuf,
9 process::{Command, Stdio},
10 time::Instant,
11};
12
13use cargo_metadata::Message;
14use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender};
15
16pub use cargo_metadata::diagnostic::{
17 Applicability, Diagnostic, DiagnosticLevel, DiagnosticSpan, DiagnosticSpanMacroExpansion,
18};
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum FlycheckConfig {
22 CargoCommand {
23 command: String,
24 all_targets: bool,
25 all_features: bool,
26 features: Vec<String>,
27 extra_args: Vec<String>,
28 },
29 CustomCommand {
30 command: String,
31 args: Vec<String>,
32 },
33}
34
35impl fmt::Display for FlycheckConfig {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command),
39 FlycheckConfig::CustomCommand { command, args } => {
40 write!(f, "{} {}", command, args.join(" "))
41 }
42 }
43 }
44}
45
46/// Flycheck wraps the shared state and communication machinery used for
47/// running `cargo check` (or other compatible command) and providing
48/// diagnostics based on the output.
49/// The spawned thread is shut down when this struct is dropped.
50#[derive(Debug)]
51pub struct FlycheckHandle {
52 // XXX: drop order is significant
53 cmd_send: Sender<CheckCommand>,
54 handle: jod_thread::JoinHandle<()>,
55}
56
57impl FlycheckHandle {
58 pub fn spawn(
59 sender: Box<dyn Fn(CheckTask) + Send>,
60 config: FlycheckConfig,
61 workspace_root: PathBuf,
62 ) -> FlycheckHandle {
63 let (cmd_send, cmd_recv) = unbounded::<CheckCommand>();
64 let handle = jod_thread::spawn(move || {
65 FlycheckActor::new(sender, config, workspace_root).run(&cmd_recv);
66 });
67 FlycheckHandle { cmd_send, handle }
68 }
69
70 /// Schedule a re-start of the cargo check worker.
71 pub fn update(&self) {
72 self.cmd_send.send(CheckCommand::Update).unwrap();
73 }
74}
75
76#[derive(Debug)]
77pub enum CheckTask {
78 /// Request a clearing of all cached diagnostics from the check watcher
79 ClearDiagnostics,
80
81 /// Request adding a diagnostic with fixes included to a file
82 AddDiagnostic { workspace_root: PathBuf, diagnostic: Diagnostic },
83
84 /// Request check progress notification to client
85 Status(Status),
86}
87
88#[derive(Debug)]
89pub enum Status {
90 Being,
91 Progress(String),
92 End,
93}
94
95pub enum CheckCommand {
96 /// Request re-start of check thread
97 Update,
98}
99
100struct FlycheckActor {
101 sender: Box<dyn Fn(CheckTask) + Send>,
102 config: FlycheckConfig,
103 workspace_root: PathBuf,
104 last_update_req: Option<Instant>,
105 // XXX: drop order is significant
106 message_recv: Receiver<CheckEvent>,
107 /// WatchThread exists to wrap around the communication needed to be able to
108 /// run `cargo check` without blocking. Currently the Rust standard library
109 /// doesn't provide a way to read sub-process output without blocking, so we
110 /// have to wrap sub-processes output handling in a thread and pass messages
111 /// back over a channel.
112 check_process: Option<jod_thread::JoinHandle<()>>,
113}
114
115impl FlycheckActor {
116 fn new(
117 sender: Box<dyn Fn(CheckTask) + Send>,
118 config: FlycheckConfig,
119 workspace_root: PathBuf,
120 ) -> FlycheckActor {
121 FlycheckActor {
122 sender,
123 config,
124 workspace_root,
125 last_update_req: None,
126 message_recv: never(),
127 check_process: None,
128 }
129 }
130
131 fn run(&mut self, cmd_recv: &Receiver<CheckCommand>) {
132 // If we rerun the thread, we need to discard the previous check results first
133 self.clean_previous_results();
134
135 loop {
136 select! {
137 recv(&cmd_recv) -> cmd => match cmd {
138 Ok(cmd) => self.handle_command(cmd),
139 Err(RecvError) => {
140 // Command channel has closed, so shut down
141 break;
142 },
143 },
144 recv(self.message_recv) -> msg => match msg {
145 Ok(msg) => self.handle_message(msg),
146 Err(RecvError) => {
147 // Watcher finished, replace it with a never channel to
148 // avoid busy-waiting.
149 self.message_recv = never();
150 self.check_process = None;
151 },
152 }
153 };
154
155 if self.should_recheck() {
156 self.last_update_req = None;
157 self.send(CheckTask::ClearDiagnostics);
158 self.restart_check_process();
159 }
160 }
161 }
162
163 fn clean_previous_results(&self) {
164 self.send(CheckTask::ClearDiagnostics);
165 self.send(CheckTask::Status(Status::End));
166 }
167
168 fn should_recheck(&mut self) -> bool {
169 if let Some(_last_update_req) = &self.last_update_req {
170 // We currently only request an update on save, as we need up to
171 // date source on disk for cargo check to do it's magic, so we
172 // don't really need to debounce the requests at this point.
173 return true;
174 }
175 false
176 }
177
178 fn handle_command(&mut self, cmd: CheckCommand) {
179 match cmd {
180 CheckCommand::Update => self.last_update_req = Some(Instant::now()),
181 }
182 }
183
184 fn handle_message(&self, msg: CheckEvent) {
185 match msg {
186 CheckEvent::Begin => {
187 self.send(CheckTask::Status(Status::Being));
188 }
189
190 CheckEvent::End => {
191 self.send(CheckTask::Status(Status::End));
192 }
193
194 CheckEvent::Msg(Message::CompilerArtifact(msg)) => {
195 self.send(CheckTask::Status(Status::Progress(msg.target.name)));
196 }
197
198 CheckEvent::Msg(Message::CompilerMessage(msg)) => {
199 self.send(CheckTask::AddDiagnostic {
200 workspace_root: self.workspace_root.clone(),
201 diagnostic: msg.message,
202 });
203 }
204
205 CheckEvent::Msg(Message::BuildScriptExecuted(_msg)) => {}
206 CheckEvent::Msg(Message::BuildFinished(_)) => {}
207 CheckEvent::Msg(Message::TextLine(_)) => {}
208 CheckEvent::Msg(Message::Unknown) => {}
209 }
210 }
211
212 fn restart_check_process(&mut self) {
213 // First, clear and cancel the old thread
214 self.message_recv = never();
215 self.check_process = None;
216
217 let mut cmd = match &self.config {
218 FlycheckConfig::CargoCommand {
219 command,
220 all_targets,
221 all_features,
222 extra_args,
223 features,
224 } => {
225 let mut cmd = Command::new(ra_toolchain::cargo());
226 cmd.arg(command);
227 cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
228 .arg(self.workspace_root.join("Cargo.toml"));
229 if *all_targets {
230 cmd.arg("--all-targets");
231 }
232 if *all_features {
233 cmd.arg("--all-features");
234 } else if !features.is_empty() {
235 cmd.arg("--features");
236 cmd.arg(features.join(" "));
237 }
238 cmd.args(extra_args);
239 cmd
240 }
241 FlycheckConfig::CustomCommand { command, args } => {
242 let mut cmd = Command::new(command);
243 cmd.args(args);
244 cmd
245 }
246 };
247 cmd.current_dir(&self.workspace_root);
248
249 let (message_send, message_recv) = unbounded();
250 self.message_recv = message_recv;
251 self.check_process = Some(jod_thread::spawn(move || {
252 // If we trigger an error here, we will do so in the loop instead,
253 // which will break out of the loop, and continue the shutdown
254 let _ = message_send.send(CheckEvent::Begin);
255
256 let res = run_cargo(cmd, &mut |message| {
257 // Skip certain kinds of messages to only spend time on what's useful
258 match &message {
259 Message::CompilerArtifact(artifact) if artifact.fresh => return true,
260 Message::BuildScriptExecuted(_) => return true,
261 Message::Unknown => return true,
262 _ => {}
263 }
264
265 // if the send channel was closed, we want to shutdown
266 message_send.send(CheckEvent::Msg(message)).is_ok()
267 });
268
269 if let Err(err) = res {
270 // FIXME: make the `message_send` to be `Sender<Result<CheckEvent, CargoError>>`
271 // to display user-caused misconfiguration errors instead of just logging them here
272 log::error!("Cargo watcher failed {:?}", err);
273 }
274
275 // We can ignore any error here, as we are already in the progress
276 // of shutting down.
277 let _ = message_send.send(CheckEvent::End);
278 }))
279 }
280
281 fn send(&self, check_task: CheckTask) {
282 (self.sender)(check_task)
283 }
284}
285
286enum CheckEvent {
287 Begin,
288 Msg(cargo_metadata::Message),
289 End,
290}
291
292fn run_cargo(
293 mut command: Command,
294 on_message: &mut dyn FnMut(cargo_metadata::Message) -> bool,
295) -> io::Result<()> {
296 let mut child =
297 command.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null()).spawn()?;
298
299 // We manually read a line at a time, instead of using serde's
300 // stream deserializers, because the deserializer cannot recover
301 // from an error, resulting in it getting stuck, because we try to
302 // be resillient against failures.
303 //
304 // Because cargo only outputs one JSON object per line, we can
305 // simply skip a line if it doesn't parse, which just ignores any
306 // erroneus output.
307 let stdout = BufReader::new(child.stdout.take().unwrap());
308 let mut read_at_least_one_message = false;
309 for message in cargo_metadata::Message::parse_stream(stdout) {
310 let message = match message {
311 Ok(message) => message,
312 Err(err) => {
313 log::error!("Invalid json from cargo check, ignoring ({})", err);
314 continue;
315 }
316 };
317
318 read_at_least_one_message = true;
319
320 if !on_message(message) {
321 break;
322 }
323 }
324
325 // It is okay to ignore the result, as it only errors if the process is already dead
326 let _ = child.kill();
327
328 let exit_status = child.wait()?;
329 if !exit_status.success() && !read_at_least_one_message {
330 // FIXME: Read the stderr to display the reason, see `read2()` reference in PR comment:
331 // https://github.com/rust-analyzer/rust-analyzer/pull/3632#discussion_r395605298
332 return Err(io::Error::new(
333 io::ErrorKind::Other,
334 format!(
335 "the command produced no valid metadata (exit code: {:?}): {:?}",
336 exit_status, command
337 ),
338 ));
339 }
340
341 Ok(())
342}