aboutsummaryrefslogtreecommitdiff
path: root/crates/flycheck/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/flycheck/src')
-rw-r--r--crates/flycheck/src/lib.rs317
1 files changed, 317 insertions, 0 deletions
diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs
new file mode 100644
index 000000000..6804d9bda
--- /dev/null
+++ b/crates/flycheck/src/lib.rs
@@ -0,0 +1,317 @@
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 ops,
9 path::PathBuf,
10 process::{self, Command, Stdio},
11 time::Duration,
12};
13
14use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
15
16pub use cargo_metadata::diagnostic::{
17 Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
18 DiagnosticSpanMacroExpansion,
19};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum FlycheckConfig {
23 CargoCommand {
24 command: String,
25 all_targets: bool,
26 all_features: bool,
27 features: Vec<String>,
28 extra_args: Vec<String>,
29 },
30 CustomCommand {
31 command: String,
32 args: Vec<String>,
33 },
34}
35
36impl fmt::Display for FlycheckConfig {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command),
40 FlycheckConfig::CustomCommand { command, args } => {
41 write!(f, "{} {}", command, args.join(" "))
42 }
43 }
44 }
45}
46
47/// Flycheck wraps the shared state and communication machinery used for
48/// running `cargo check` (or other compatible command) and providing
49/// diagnostics based on the output.
50/// The spawned thread is shut down when this struct is dropped.
51#[derive(Debug)]
52pub struct FlycheckHandle {
53 // XXX: drop order is significant
54 sender: Sender<Restart>,
55 thread: jod_thread::JoinHandle,
56}
57
58impl FlycheckHandle {
59 pub fn spawn(
60 sender: Box<dyn Fn(Message) + Send>,
61 config: FlycheckConfig,
62 workspace_root: PathBuf,
63 ) -> FlycheckHandle {
64 let actor = FlycheckActor::new(sender, config, workspace_root);
65 let (sender, receiver) = unbounded::<Restart>();
66 let thread = jod_thread::spawn(move || actor.run(receiver));
67 FlycheckHandle { sender, thread }
68 }
69
70 /// Schedule a re-start of the cargo check worker.
71 pub fn update(&self) {
72 self.sender.send(Restart).unwrap();
73 }
74}
75
76#[derive(Debug)]
77pub enum Message {
78 /// Request adding a diagnostic with fixes included to a file
79 AddDiagnostic { workspace_root: PathBuf, diagnostic: Diagnostic },
80
81 /// Request check progress notification to client
82 Progress(Progress),
83}
84
85#[derive(Debug)]
86pub enum Progress {
87 DidStart,
88 DidCheckCrate(String),
89 DidFinish(io::Result<()>),
90 DidCancel,
91}
92
93struct Restart;
94
95struct FlycheckActor {
96 sender: Box<dyn Fn(Message) + Send>,
97 config: FlycheckConfig,
98 workspace_root: PathBuf,
99 /// WatchThread exists to wrap around the communication needed to be able to
100 /// run `cargo check` without blocking. Currently the Rust standard library
101 /// doesn't provide a way to read sub-process output without blocking, so we
102 /// have to wrap sub-processes output handling in a thread and pass messages
103 /// back over a channel.
104 cargo_handle: Option<CargoHandle>,
105}
106
107enum Event {
108 Restart(Restart),
109 CheckEvent(Option<cargo_metadata::Message>),
110}
111
112impl FlycheckActor {
113 fn new(
114 sender: Box<dyn Fn(Message) + Send>,
115 config: FlycheckConfig,
116 workspace_root: PathBuf,
117 ) -> FlycheckActor {
118 FlycheckActor { sender, config, workspace_root, cargo_handle: None }
119 }
120 fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
121 let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
122 select! {
123 recv(inbox) -> msg => msg.ok().map(Event::Restart),
124 recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
125 }
126 }
127 fn run(mut self, inbox: Receiver<Restart>) {
128 while let Some(event) = self.next_event(&inbox) {
129 match event {
130 Event::Restart(Restart) => {
131 while let Ok(Restart) = inbox.recv_timeout(Duration::from_millis(50)) {}
132
133 self.cancel_check_process();
134
135 let mut command = self.check_command();
136 log::info!("restart flycheck {:?}", command);
137 command.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null());
138 if let Ok(child) = command.spawn().map(JodChild) {
139 self.cargo_handle = Some(CargoHandle::spawn(child));
140 self.send(Message::Progress(Progress::DidStart));
141 }
142 }
143 Event::CheckEvent(None) => {
144 // Watcher finished, replace it with a never channel to
145 // avoid busy-waiting.
146 let cargo_handle = self.cargo_handle.take().unwrap();
147 let res = cargo_handle.join();
148 self.send(Message::Progress(Progress::DidFinish(res)));
149 }
150 Event::CheckEvent(Some(message)) => match message {
151 cargo_metadata::Message::CompilerArtifact(msg) => {
152 self.send(Message::Progress(Progress::DidCheckCrate(msg.target.name)));
153 }
154
155 cargo_metadata::Message::CompilerMessage(msg) => {
156 self.send(Message::AddDiagnostic {
157 workspace_root: self.workspace_root.clone(),
158 diagnostic: msg.message,
159 });
160 }
161
162 cargo_metadata::Message::BuildScriptExecuted(_)
163 | cargo_metadata::Message::BuildFinished(_)
164 | cargo_metadata::Message::TextLine(_)
165 | cargo_metadata::Message::Unknown => {}
166 },
167 }
168 }
169 // If we rerun the thread, we need to discard the previous check results first
170 self.cancel_check_process();
171 }
172 fn cancel_check_process(&mut self) {
173 if self.cargo_handle.take().is_some() {
174 self.send(Message::Progress(Progress::DidCancel));
175 }
176 }
177 fn check_command(&self) -> Command {
178 let mut cmd = match &self.config {
179 FlycheckConfig::CargoCommand {
180 command,
181 all_targets,
182 all_features,
183 extra_args,
184 features,
185 } => {
186 let mut cmd = Command::new(ra_toolchain::cargo());
187 cmd.arg(command);
188 cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
189 .arg(self.workspace_root.join("Cargo.toml"));
190 if *all_targets {
191 cmd.arg("--all-targets");
192 }
193 if *all_features {
194 cmd.arg("--all-features");
195 } else if !features.is_empty() {
196 cmd.arg("--features");
197 cmd.arg(features.join(" "));
198 }
199 cmd.args(extra_args);
200 cmd
201 }
202 FlycheckConfig::CustomCommand { command, args } => {
203 let mut cmd = Command::new(command);
204 cmd.args(args);
205 cmd
206 }
207 };
208 cmd.current_dir(&self.workspace_root);
209 cmd
210 }
211
212 fn send(&self, check_task: Message) {
213 (self.sender)(check_task)
214 }
215}
216
217struct CargoHandle {
218 child: JodChild,
219 #[allow(unused)]
220 thread: jod_thread::JoinHandle<io::Result<bool>>,
221 receiver: Receiver<cargo_metadata::Message>,
222}
223
224impl CargoHandle {
225 fn spawn(mut child: JodChild) -> CargoHandle {
226 let child_stdout = child.stdout.take().unwrap();
227 let (sender, receiver) = unbounded();
228 let actor = CargoActor::new(child_stdout, sender);
229 let thread = jod_thread::spawn(move || actor.run());
230 CargoHandle { child, thread, receiver }
231 }
232 fn join(mut self) -> io::Result<()> {
233 // It is okay to ignore the result, as it only errors if the process is already dead
234 let _ = self.child.kill();
235 let exit_status = self.child.wait()?;
236 let read_at_least_one_message = self.thread.join()?;
237 if !exit_status.success() && !read_at_least_one_message {
238 // FIXME: Read the stderr to display the reason, see `read2()` reference in PR comment:
239 // https://github.com/rust-analyzer/rust-analyzer/pull/3632#discussion_r395605298
240 return Err(io::Error::new(
241 io::ErrorKind::Other,
242 format!(
243 "Cargo watcher failed,the command produced no valid metadata (exit code: {:?})",
244 exit_status
245 ),
246 ));
247 }
248 Ok(())
249 }
250}
251
252struct CargoActor {
253 child_stdout: process::ChildStdout,
254 sender: Sender<cargo_metadata::Message>,
255}
256
257impl CargoActor {
258 fn new(
259 child_stdout: process::ChildStdout,
260 sender: Sender<cargo_metadata::Message>,
261 ) -> CargoActor {
262 CargoActor { child_stdout, sender }
263 }
264 fn run(self) -> io::Result<bool> {
265 // We manually read a line at a time, instead of using serde's
266 // stream deserializers, because the deserializer cannot recover
267 // from an error, resulting in it getting stuck, because we try to
268 // be resilient against failures.
269 //
270 // Because cargo only outputs one JSON object per line, we can
271 // simply skip a line if it doesn't parse, which just ignores any
272 // erroneus output.
273 let stdout = BufReader::new(self.child_stdout);
274 let mut read_at_least_one_message = false;
275 for message in cargo_metadata::Message::parse_stream(stdout) {
276 let message = match message {
277 Ok(message) => message,
278 Err(err) => {
279 log::error!("Invalid json from cargo check, ignoring ({})", err);
280 continue;
281 }
282 };
283
284 read_at_least_one_message = true;
285
286 // Skip certain kinds of messages to only spend time on what's useful
287 match &message {
288 cargo_metadata::Message::CompilerArtifact(artifact) if artifact.fresh => (),
289 cargo_metadata::Message::BuildScriptExecuted(_)
290 | cargo_metadata::Message::Unknown => (),
291 _ => self.sender.send(message).unwrap(),
292 }
293 }
294 Ok(read_at_least_one_message)
295 }
296}
297
298struct JodChild(process::Child);
299
300impl ops::Deref for JodChild {
301 type Target = process::Child;
302 fn deref(&self) -> &process::Child {
303 &self.0
304 }
305}
306
307impl ops::DerefMut for JodChild {
308 fn deref_mut(&mut self) -> &mut process::Child {
309 &mut self.0
310 }
311}
312
313impl Drop for JodChild {
314 fn drop(&mut self) {
315 let _ = self.0.kill();
316 }
317}