diff options
Diffstat (limited to '')
-rw-r--r-- | src/tools/rust-analyzer/crates/stdx/Cargo.toml | 24 | ||||
-rw-r--r-- | src/tools/rust-analyzer/crates/stdx/src/lib.rs | 247 | ||||
-rw-r--r-- | src/tools/rust-analyzer/crates/stdx/src/macros.rs | 47 | ||||
-rw-r--r-- | src/tools/rust-analyzer/crates/stdx/src/non_empty_vec.rs | 39 | ||||
-rw-r--r-- | src/tools/rust-analyzer/crates/stdx/src/panic_context.rs | 49 | ||||
-rw-r--r-- | src/tools/rust-analyzer/crates/stdx/src/process.rs | 267 |
6 files changed, 673 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/stdx/Cargo.toml b/src/tools/rust-analyzer/crates/stdx/Cargo.toml new file mode 100644 index 000000000..092b99ae5 --- /dev/null +++ b/src/tools/rust-analyzer/crates/stdx/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "stdx" +version = "0.0.0" +description = "TBD" +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.57" + +[lib] +doctest = false + +[dependencies] +libc = "0.2.126" +backtrace = { version = "0.3.65", optional = true } +always-assert = { version = "0.1.2", features = ["log"] } +# Think twice before adding anything here + +[target.'cfg(windows)'.dependencies] +miow = "0.4.0" +winapi = { version = "0.3.9", features = ["winerror"] } + +[features] +# Uncomment to enable for the whole crate graph +# default = [ "backtrace" ] diff --git a/src/tools/rust-analyzer/crates/stdx/src/lib.rs b/src/tools/rust-analyzer/crates/stdx/src/lib.rs new file mode 100644 index 000000000..b4d45206c --- /dev/null +++ b/src/tools/rust-analyzer/crates/stdx/src/lib.rs @@ -0,0 +1,247 @@ +//! Missing batteries for standard libraries. + +#![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)] + +use std::process::Command; +use std::{cmp::Ordering, ops, time::Instant}; +use std::{io as sio, iter}; + +mod macros; +pub mod process; +pub mod panic_context; +pub mod non_empty_vec; + +pub use always_assert::{always, never}; + +#[inline(always)] +pub fn is_ci() -> bool { + option_env!("CI").is_some() +} + +#[must_use] +pub fn timeit(label: &'static str) -> impl Drop { + let start = Instant::now(); + defer(move || eprintln!("{}: {:.2?}", label, start.elapsed())) +} + +/// Prints backtrace to stderr, useful for debugging. +pub fn print_backtrace() { + #[cfg(feature = "backtrace")] + eprintln!("{:?}", backtrace::Backtrace::new()); + + #[cfg(not(feature = "backtrace"))] + eprintln!( + r#"Enable the backtrace feature. +Uncomment `default = [ "backtrace" ]` in `crates/stdx/Cargo.toml`. +"# + ); +} + +pub fn to_lower_snake_case(s: &str) -> String { + to_snake_case(s, char::to_ascii_lowercase) +} +pub fn to_upper_snake_case(s: &str) -> String { + to_snake_case(s, char::to_ascii_uppercase) +} + +// Code partially taken from rust/compiler/rustc_lint/src/nonstandard_style.rs +// commit: 9626f2b +fn to_snake_case<F: Fn(&char) -> char>(mut s: &str, change_case: F) -> String { + let mut words = vec![]; + + // Preserve leading underscores + s = s.trim_start_matches(|c: char| { + if c == '_' { + words.push(String::new()); + true + } else { + false + } + }); + + for s in s.split('_') { + let mut last_upper = false; + let mut buf = String::new(); + + if s.is_empty() { + continue; + } + + for ch in s.chars() { + if !buf.is_empty() && buf != "'" && ch.is_uppercase() && !last_upper { + words.push(buf); + buf = String::new(); + } + + last_upper = ch.is_uppercase(); + buf.extend(iter::once(change_case(&ch))); + } + + words.push(buf); + } + + words.join("_") +} + +pub fn replace(buf: &mut String, from: char, to: &str) { + if !buf.contains(from) { + return; + } + // FIXME: do this in place. + *buf = buf.replace(from, to); +} + +pub fn trim_indent(mut text: &str) -> String { + if text.starts_with('\n') { + text = &text[1..]; + } + let indent = text + .lines() + .filter(|it| !it.trim().is_empty()) + .map(|it| it.len() - it.trim_start().len()) + .min() + .unwrap_or(0); + text.split_inclusive('\n') + .map( + |line| { + if line.len() <= indent { + line.trim_start_matches(' ') + } else { + &line[indent..] + } + }, + ) + .collect() +} + +pub fn equal_range_by<T, F>(slice: &[T], mut key: F) -> ops::Range<usize> +where + F: FnMut(&T) -> Ordering, +{ + let start = slice.partition_point(|it| key(it) == Ordering::Less); + let len = slice[start..].partition_point(|it| key(it) == Ordering::Equal); + start..start + len +} + +#[must_use] +pub fn defer<F: FnOnce()>(f: F) -> impl Drop { + struct D<F: FnOnce()>(Option<F>); + impl<F: FnOnce()> Drop for D<F> { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + D(Some(f)) +} + +/// A [`std::process::Child`] wrapper that will kill the child on drop. +#[cfg_attr(not(target_arch = "wasm32"), repr(transparent))] +#[derive(Debug)] +pub struct JodChild(pub std::process::Child); + +impl ops::Deref for JodChild { + type Target = std::process::Child; + fn deref(&self) -> &std::process::Child { + &self.0 + } +} + +impl ops::DerefMut for JodChild { + fn deref_mut(&mut self) -> &mut std::process::Child { + &mut self.0 + } +} + +impl Drop for JodChild { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} + +impl JodChild { + pub fn spawn(mut command: Command) -> sio::Result<Self> { + command.spawn().map(Self) + } + + pub fn into_inner(self) -> std::process::Child { + if cfg!(target_arch = "wasm32") { + panic!("no processes on wasm"); + } + // SAFETY: repr transparent, except on WASM + unsafe { std::mem::transmute::<JodChild, std::process::Child>(self) } + } +} + +// feature: iter_order_by +// Iterator::eq_by +pub fn iter_eq_by<I, I2, F>(this: I2, other: I, mut eq: F) -> bool +where + I: IntoIterator, + I2: IntoIterator, + F: FnMut(I2::Item, I::Item) -> bool, +{ + let mut other = other.into_iter(); + let mut this = this.into_iter(); + + loop { + let x = match this.next() { + None => return other.next().is_none(), + Some(val) => val, + }; + + let y = match other.next() { + None => return false, + Some(val) => val, + }; + + if !eq(x, y) { + return false; + } + } +} + +/// Returns all final segments of the argument, longest first. +pub fn slice_tails<T>(this: &[T]) -> impl Iterator<Item = &[T]> { + (0..this.len()).map(|i| &this[i..]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_trim_indent() { + assert_eq!(trim_indent(""), ""); + assert_eq!( + trim_indent( + " + hello + world +" + ), + "hello\nworld\n" + ); + assert_eq!( + trim_indent( + " + hello + world" + ), + "hello\nworld" + ); + assert_eq!(trim_indent(" hello\n world\n"), "hello\nworld\n"); + assert_eq!( + trim_indent( + " + fn main() { + return 92; + } + " + ), + "fn main() {\n return 92;\n}\n" + ); + } +} diff --git a/src/tools/rust-analyzer/crates/stdx/src/macros.rs b/src/tools/rust-analyzer/crates/stdx/src/macros.rs new file mode 100644 index 000000000..d91fc690c --- /dev/null +++ b/src/tools/rust-analyzer/crates/stdx/src/macros.rs @@ -0,0 +1,47 @@ +//! Convenience macros. + +#[macro_export] +macro_rules! eprintln { + ($($tt:tt)*) => {{ + if $crate::is_ci() { + panic!("Forgot to remove debug-print?") + } + std::eprintln!($($tt)*) + }} +} + +/// Appends formatted string to a `String`. +#[macro_export] +macro_rules! format_to { + ($buf:expr) => (); + ($buf:expr, $lit:literal $($arg:tt)*) => { + { use ::std::fmt::Write as _; let _ = ::std::write!($buf, $lit $($arg)*); } + }; +} + +/// Generates `From` impls for `Enum E { Foo(Foo), Bar(Bar) }` enums +/// +/// # Example +/// +/// ```rust +/// impl_from!(Struct, Union, Enum for Adt); +/// ``` +#[macro_export] +macro_rules! impl_from { + ($($variant:ident $(($($sub_variant:ident),*))?),* for $enum:ident) => { + $( + impl From<$variant> for $enum { + fn from(it: $variant) -> $enum { + $enum::$variant(it) + } + } + $($( + impl From<$sub_variant> for $enum { + fn from(it: $sub_variant) -> $enum { + $enum::$variant($variant::$sub_variant(it)) + } + } + )*)? + )* + } +} diff --git a/src/tools/rust-analyzer/crates/stdx/src/non_empty_vec.rs b/src/tools/rust-analyzer/crates/stdx/src/non_empty_vec.rs new file mode 100644 index 000000000..342194c78 --- /dev/null +++ b/src/tools/rust-analyzer/crates/stdx/src/non_empty_vec.rs @@ -0,0 +1,39 @@ +//! See [`NonEmptyVec`]. + +/// A [`Vec`] that is guaranteed to at least contain one element. +pub struct NonEmptyVec<T> { + first: T, + rest: Vec<T>, +} + +impl<T> NonEmptyVec<T> { + #[inline] + pub fn new(first: T) -> Self { + NonEmptyVec { first, rest: Vec::new() } + } + + #[inline] + pub fn last_mut(&mut self) -> &mut T { + self.rest.last_mut().unwrap_or(&mut self.first) + } + + #[inline] + pub fn pop(&mut self) -> Option<T> { + self.rest.pop() + } + + #[inline] + pub fn push(&mut self, value: T) { + self.rest.push(value) + } + + #[inline] + pub fn len(&self) -> usize { + 1 + self.rest.len() + } + + #[inline] + pub fn into_last(mut self) -> T { + self.rest.pop().unwrap_or(self.first) + } +} diff --git a/src/tools/rust-analyzer/crates/stdx/src/panic_context.rs b/src/tools/rust-analyzer/crates/stdx/src/panic_context.rs new file mode 100644 index 000000000..f8fafc5a6 --- /dev/null +++ b/src/tools/rust-analyzer/crates/stdx/src/panic_context.rs @@ -0,0 +1,49 @@ +//! A micro-crate to enhance panic messages with context info. +//! +//! FIXME: upstream to <https://github.com/kriomant/panic-context> ? + +use std::{cell::RefCell, panic, sync::Once}; + +pub fn enter(context: String) -> PanicContext { + static ONCE: Once = Once::new(); + ONCE.call_once(PanicContext::init); + + with_ctx(|ctx| ctx.push(context)); + PanicContext { _priv: () } +} + +#[must_use] +pub struct PanicContext { + _priv: (), +} + +impl PanicContext { + fn init() { + let default_hook = panic::take_hook(); + let hook = move |panic_info: &panic::PanicInfo<'_>| { + with_ctx(|ctx| { + if !ctx.is_empty() { + eprintln!("Panic context:"); + for frame in ctx.iter() { + eprintln!("> {}\n", frame); + } + } + default_hook(panic_info); + }); + }; + panic::set_hook(Box::new(hook)); + } +} + +impl Drop for PanicContext { + fn drop(&mut self) { + with_ctx(|ctx| assert!(ctx.pop().is_some())); + } +} + +fn with_ctx(f: impl FnOnce(&mut Vec<String>)) { + thread_local! { + static CTX: RefCell<Vec<String>> = RefCell::new(Vec::new()); + } + CTX.with(|ctx| f(&mut *ctx.borrow_mut())); +} diff --git a/src/tools/rust-analyzer/crates/stdx/src/process.rs b/src/tools/rust-analyzer/crates/stdx/src/process.rs new file mode 100644 index 000000000..e5aa34365 --- /dev/null +++ b/src/tools/rust-analyzer/crates/stdx/src/process.rs @@ -0,0 +1,267 @@ +//! Read both stdout and stderr of child without deadlocks. +//! +//! <https://github.com/rust-lang/cargo/blob/905af549966f23a9288e9993a85d1249a5436556/crates/cargo-util/src/read2.rs> +//! <https://github.com/rust-lang/cargo/blob/58a961314437258065e23cb6316dfc121d96fb71/crates/cargo-util/src/process_builder.rs#L231> + +use std::{ + io, + process::{ChildStderr, ChildStdout, Command, Output, Stdio}, +}; + +use crate::JodChild; + +pub fn streaming_output( + out: ChildStdout, + err: ChildStderr, + on_stdout_line: &mut dyn FnMut(&str), + on_stderr_line: &mut dyn FnMut(&str), +) -> io::Result<(Vec<u8>, Vec<u8>)> { + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + + imp::read2(out, err, &mut |is_out, data, eof| { + let idx = if eof { + data.len() + } else { + match data.iter().rposition(|b| *b == b'\n') { + Some(i) => i + 1, + None => return, + } + }; + { + // scope for new_lines + let new_lines = { + let dst = if is_out { &mut stdout } else { &mut stderr }; + let start = dst.len(); + let data = data.drain(..idx); + dst.extend(data); + &dst[start..] + }; + for line in String::from_utf8_lossy(new_lines).lines() { + if is_out { + on_stdout_line(line); + } else { + on_stderr_line(line); + } + } + } + })?; + + Ok((stdout, stderr)) +} + +pub fn spawn_with_streaming_output( + mut cmd: Command, + on_stdout_line: &mut dyn FnMut(&str), + on_stderr_line: &mut dyn FnMut(&str), +) -> io::Result<Output> { + let cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null()); + + let mut child = JodChild(cmd.spawn()?); + let (stdout, stderr) = streaming_output( + child.stdout.take().unwrap(), + child.stderr.take().unwrap(), + on_stdout_line, + on_stderr_line, + )?; + let status = child.wait()?; + Ok(Output { status, stdout, stderr }) +} + +#[cfg(unix)] +mod imp { + use std::{ + io::{self, prelude::*}, + mem, + os::unix::prelude::*, + process::{ChildStderr, ChildStdout}, + }; + + pub(crate) fn read2( + mut out_pipe: ChildStdout, + mut err_pipe: ChildStderr, + data: &mut dyn FnMut(bool, &mut Vec<u8>, bool), + ) -> io::Result<()> { + unsafe { + libc::fcntl(out_pipe.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK); + libc::fcntl(err_pipe.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK); + } + + let mut out_done = false; + let mut err_done = false; + let mut out = Vec::new(); + let mut err = Vec::new(); + + let mut fds: [libc::pollfd; 2] = unsafe { mem::zeroed() }; + fds[0].fd = out_pipe.as_raw_fd(); + fds[0].events = libc::POLLIN; + fds[1].fd = err_pipe.as_raw_fd(); + fds[1].events = libc::POLLIN; + let mut nfds = 2; + let mut errfd = 1; + + while nfds > 0 { + // wait for either pipe to become readable using `select` + let r = unsafe { libc::poll(fds.as_mut_ptr(), nfds, -1) }; + if r == -1 { + let err = io::Error::last_os_error(); + if err.kind() == io::ErrorKind::Interrupted { + continue; + } + return Err(err); + } + + // Read as much as we can from each pipe, ignoring EWOULDBLOCK or + // EAGAIN. If we hit EOF, then this will happen because the underlying + // reader will return Ok(0), in which case we'll see `Ok` ourselves. In + // this case we flip the other fd back into blocking mode and read + // whatever's leftover on that file descriptor. + let handle = |res: io::Result<_>| match res { + Ok(_) => Ok(true), + Err(e) => { + if e.kind() == io::ErrorKind::WouldBlock { + Ok(false) + } else { + Err(e) + } + } + }; + if !err_done && fds[errfd].revents != 0 && handle(err_pipe.read_to_end(&mut err))? { + err_done = true; + nfds -= 1; + } + data(false, &mut err, err_done); + if !out_done && fds[0].revents != 0 && handle(out_pipe.read_to_end(&mut out))? { + out_done = true; + fds[0].fd = err_pipe.as_raw_fd(); + errfd = 0; + nfds -= 1; + } + data(true, &mut out, out_done); + } + Ok(()) + } +} + +#[cfg(windows)] +mod imp { + use std::{ + io, + os::windows::prelude::*, + process::{ChildStderr, ChildStdout}, + slice, + }; + + use miow::{ + iocp::{CompletionPort, CompletionStatus}, + pipe::NamedPipe, + Overlapped, + }; + use winapi::shared::winerror::ERROR_BROKEN_PIPE; + + struct Pipe<'a> { + dst: &'a mut Vec<u8>, + overlapped: Overlapped, + pipe: NamedPipe, + done: bool, + } + + pub(crate) fn read2( + out_pipe: ChildStdout, + err_pipe: ChildStderr, + data: &mut dyn FnMut(bool, &mut Vec<u8>, bool), + ) -> io::Result<()> { + let mut out = Vec::new(); + let mut err = Vec::new(); + + let port = CompletionPort::new(1)?; + port.add_handle(0, &out_pipe)?; + port.add_handle(1, &err_pipe)?; + + unsafe { + let mut out_pipe = Pipe::new(out_pipe, &mut out); + let mut err_pipe = Pipe::new(err_pipe, &mut err); + + out_pipe.read()?; + err_pipe.read()?; + + let mut status = [CompletionStatus::zero(), CompletionStatus::zero()]; + + while !out_pipe.done || !err_pipe.done { + for status in port.get_many(&mut status, None)? { + if status.token() == 0 { + out_pipe.complete(status); + data(true, out_pipe.dst, out_pipe.done); + out_pipe.read()?; + } else { + err_pipe.complete(status); + data(false, err_pipe.dst, err_pipe.done); + err_pipe.read()?; + } + } + } + + Ok(()) + } + } + + impl<'a> Pipe<'a> { + unsafe fn new<P: IntoRawHandle>(p: P, dst: &'a mut Vec<u8>) -> Pipe<'a> { + Pipe { + dst, + pipe: NamedPipe::from_raw_handle(p.into_raw_handle()), + overlapped: Overlapped::zero(), + done: false, + } + } + + unsafe fn read(&mut self) -> io::Result<()> { + let dst = slice_to_end(self.dst); + match self.pipe.read_overlapped(dst, self.overlapped.raw()) { + Ok(_) => Ok(()), + Err(e) => { + if e.raw_os_error() == Some(ERROR_BROKEN_PIPE as i32) { + self.done = true; + Ok(()) + } else { + Err(e) + } + } + } + } + + unsafe fn complete(&mut self, status: &CompletionStatus) { + let prev = self.dst.len(); + self.dst.set_len(prev + status.bytes_transferred() as usize); + if status.bytes_transferred() == 0 { + self.done = true; + } + } + } + + unsafe fn slice_to_end(v: &mut Vec<u8>) -> &mut [u8] { + if v.capacity() == 0 { + v.reserve(16); + } + if v.capacity() == v.len() { + v.reserve(1); + } + slice::from_raw_parts_mut(v.as_mut_ptr().add(v.len()), v.capacity() - v.len()) + } +} + +#[cfg(target_arch = "wasm32")] +mod imp { + use std::{ + io, + process::{ChildStderr, ChildStdout}, + }; + + pub(crate) fn read2( + _out_pipe: ChildStdout, + _err_pipe: ChildStderr, + _data: &mut dyn FnMut(bool, &mut Vec<u8>, bool), + ) -> io::Result<()> { + panic!("no processes on wasm") + } +} |