#![deny(missing_docs)]
use std::{
collections::VecDeque,
error,
fmt::{self, Debug, Display, Formatter},
fs,
io::{self, Read, Write},
path::PathBuf,
process::{Child, Command, Stdio},
result,
};
use colored::Colorize;
use pad::{Alignment, PadStr};
use serde_derive::{Deserialize, Serialize};
#[derive(Debug)]
pub enum Error {
Cargo,
IO(io::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use Error::*;
match self {
Cargo => write!(f, "Unable to run cargo"),
IO(e) => write!(f, "{}", e),
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::IO(e)
}
}
impl error::Error for Error {}
pub type Result<T> = result::Result<T, Error>;
pub fn terminal_width() -> usize {
terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.unwrap_or(100)
}
const LEVEL_COLUMN_WIDTH: usize = 7;
const FILE_COLUMN_WIDTH: usize = 18;
const LINE_COLUMN_WIDTH: usize = 8;
const ELIPSES_COLUMN_WIDTH: usize = 3;
fn message_column_width(terminal_width: usize) -> usize {
terminal_width - LEVEL_COLUMN_WIDTH - FILE_COLUMN_WIDTH - LINE_COLUMN_WIDTH - 6
}
fn ensure_color() {
#[cfg(windows)]
colored::control::set_virtual_terminal(true).unwrap();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum Checker {
Check,
Clippy,
Build,
}
impl Default for Checker {
fn default() -> Self {
Checker::Check
}
}
pub struct Analyzer {
child: Child,
buffer: VecDeque<u8>,
debug: bool,
color: bool,
}
impl Analyzer {
pub fn new() -> Result<Analyzer> {
Analyzer::with_args(Checker::Check, &[])
}
pub fn clippy() -> Result<Analyzer> {
Analyzer::with_args(Checker::Clippy, &[])
}
pub fn with_args(checker: Checker, args: &[String]) -> Result<Analyzer> {
ensure_color();
Ok(Analyzer {
child: Command::new("cargo")
.args(&[
&format!("{:?}", checker).to_lowercase(),
"--message-format",
"json",
])
.args(args)
.stdin(Stdio::null())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.map_err(|_| Error::Cargo)?,
buffer: VecDeque::new(),
debug: false,
color: true,
})
}
pub fn debug(self, debug: bool) -> Self {
if debug {
let _ = fs::write("coral.json", &[]);
}
Analyzer { debug, ..self }
}
pub fn color(self, color: bool) -> Self {
Analyzer { color, ..self }
}
fn add_to_buffer(&mut self) {
const BUFFER_LEN: usize = 100;
let mut buffer = [0u8; BUFFER_LEN];
while !self.buffer.contains(&b'\n') {
if let Ok(len) = self.child.stdout.as_mut().unwrap().read(&mut buffer) {
if len == 0 {
break;
} else {
self.buffer.extend(&buffer[..len]);
}
} else {
break;
}
}
}
}
impl Iterator for Analyzer {
type Item = Entry;
fn next(&mut self) -> Option<Self::Item> {
colored::control::set_override(true);
self.add_to_buffer();
let mut entry_buffer = Vec::new();
while let Some(byte) = self.buffer.pop_front().filter(|&b| b != b'\n') {
entry_buffer.push(byte);
}
let res = if entry_buffer.is_empty() {
None
} else {
if self.debug {
println!("\t{}\n", String::from_utf8_lossy(&entry_buffer));
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open("coral.json")
.unwrap();
let _ = file.write(&entry_buffer).unwrap();
writeln!(file).unwrap();
}
let mut entry: Entry = serde_json::from_slice(&entry_buffer).unwrap();
entry.color = self.color;
Some(entry)
};
if res.is_none() {
self.child.wait().unwrap();
}
res
}
}
impl Debug for Analyzer {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Analyzer")
}
}
fn default_color_setting() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[allow(missing_docs)]
pub struct Entry {
pub reason: Reason,
pub package_id: String,
pub target: Option<Target>,
pub message: Option<Message>,
pub profile: Option<Profile>,
pub features: Option<Vec<String>>,
pub filenames: Option<Vec<PathBuf>>,
pub executable: Option<PathBuf>,
pub fresh: Option<bool>,
#[serde(default = "default_color_setting")]
pub color: bool,
}
impl Entry {
pub fn is_message(&self) -> bool {
self.reason == Reason::CompilerMessage
}
pub fn is_artifact(&self) -> bool {
self.reason == Reason::CompilerArtifact
}
pub fn is_warning(&self) -> bool {
self.message
.as_ref()
.map(Message::is_warning)
.unwrap_or(false)
}
pub fn is_error(&self) -> bool {
self.message
.as_ref()
.map(Message::is_error)
.unwrap_or(false)
}
pub fn is_note(&self) -> bool {
self.message.as_ref().map(Message::is_note).unwrap_or(false)
}
pub fn is_help(&self) -> bool {
self.message.as_ref().map(Message::is_help).unwrap_or(false)
}
pub fn report(&self) -> Option<String> {
self.report_width(terminal_width())
}
pub fn report_width(&self, terminal_width: usize) -> Option<String> {
self.message
.as_ref()
.and_then(|m| m.report(self.color, terminal_width))
}
pub fn rendered(&self) -> Option<&str> {
self.message
.as_ref()
.and_then(|m| m.rendered.as_ref().map(String::as_str))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(missing_docs)]
pub enum Reason {
CompilerArtifact,
CompilerMessage,
BuildScriptExecuted,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[allow(missing_docs)]
pub struct Target {
pub kind: Vec<TargetKind>,
pub crate_types: Vec<CrateType>,
pub name: String,
pub src_path: PathBuf,
pub edition: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(missing_docs)]
pub enum TargetKind {
Lib,
Bin,
Rlib,
CustomBuild,
ProcMacro,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[allow(missing_docs)]
pub struct Message {
pub message: String,
pub code: Option<Code>,
pub level: Level,
pub spans: Option<Vec<Span>>,
pub children: Option<Vec<Message>>,
pub rendered: Option<String>,
}
impl Message {
pub fn is_warning(&self) -> bool {
self.level.is_warning()
}
pub fn is_error(&self) -> bool {
self.level.is_error()
}
pub fn is_note(&self) -> bool {
self.level.is_note()
}
pub fn is_help(&self) -> bool {
self.level.is_help()
}
pub fn report_headers(color: bool) -> String {
ensure_color();
colored::control::set_override(color);
let level = "Level"
.pad_to_width_with_alignment(LEVEL_COLUMN_WIDTH, Alignment::Right)
.bright_white();
let file = "File"
.pad_to_width_with_alignment(FILE_COLUMN_WIDTH, Alignment::Right)
.bright_white();
let line = "Line"
.pad_to_width_with_alignment(LINE_COLUMN_WIDTH, Alignment::Left)
.bright_white();
let message = "Message".bright_white();
let res = format!("{} {} {} {}", level, file, line, message);
colored::control::unset_override();
res
}
pub fn report(&self, color: bool, terminal_width: usize) -> Option<String> {
if self.message.contains("aborting") {
None
} else if self.level.is_some() {
let span = self.spans.as_ref().and_then(|v| v.last());
colored::control::set_override(color);
let level = self.level.format();
let file = span
.as_ref()
.map(|span| span.file_name_string())
.unwrap_or_else(String::new);
let file = if file.len() <= FILE_COLUMN_WIDTH {
file
} else {
format!("...{}", &file[(file.len() - FILE_COLUMN_WIDTH + 3)..])
}
.pad_to_width_with_alignment(FILE_COLUMN_WIDTH, Alignment::Right)
.bright_cyan();
let line = if let Some(ref span) = span {
let (line, column) = span.line();
format!("{}:{}", line, column)
} else {
String::new()
}
.pad_to_width_with_alignment(LINE_COLUMN_WIDTH, Alignment::Left)
.bright_cyan();
let message_column_width = message_column_width(terminal_width);
let mut message = self.message.clone();
message.retain(|c| c != '\n');
let message = if message.len() <= message_column_width {
message[..(message_column_width.min(message.len()))].to_string()
} else {
format!(
"{}...",
&message[..((message_column_width - ELIPSES_COLUMN_WIDTH).min(message.len()))]
)
}
.pad_to_width_with_alignment(message_column_width, Alignment::Left)
.white();
let res = Some(format!(
"{} {} {} {} {}",
level,
file,
if span.is_some() { "at" } else { " " },
line,
message
));
colored::control::unset_override();
res
} else {
None
}
}
pub fn replacement_span(&self) -> Option<&Span> {
self.spans
.as_ref()
.and_then(|spans| {
spans
.iter()
.find(|span| span.suggested_replacement.is_some())
})
.or_else(|| {
self.children
.as_ref()
.and_then(|children| children.iter().find_map(Message::replacement_span))
})
}
pub fn unroll(&self) -> impl Iterator<Item = &Message> {
let mut messages = Vec::new();
messages.push(self);
if let Some(ref children) = self.children {
for child in children {
messages.extend(child.unroll());
}
}
messages.into_iter()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[allow(missing_docs)]
pub struct Code {
pub code: String,
pub explanation: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(missing_docs)]
pub enum Level {
#[serde(rename = "")]
None,
Note,
Help,
Warning,
Error,
}
impl Level {
pub fn is_warning(self) -> bool {
self == Level::Warning
}
pub fn is_error(self) -> bool {
self == Level::Error
}
pub fn is_note(self) -> bool {
self == Level::Note
}
pub fn is_help(self) -> bool {
self == Level::Help
}
pub fn is_some(self) -> bool {
!self.is_none()
}
pub fn is_none(self) -> bool {
self == Level::None
}
fn format(self) -> String {
let pad = |s: &str| s.pad_to_width_with_alignment(LEVEL_COLUMN_WIDTH, Alignment::Right);
match self {
Level::None => String::new(),
Level::Note => format!("{}", pad("note").bright_cyan()),
Level::Help => format!("{}", pad("help").bright_green()),
Level::Warning => format!("{}", pad("warning").bright_yellow()),
Level::Error => format!("{}", pad("error").bright_red()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[allow(missing_docs)]
pub struct Span {
pub file_name: PathBuf,
pub byte_start: usize,
pub byte_end: usize,
pub line_start: usize,
pub line_end: usize,
pub column_start: usize,
pub column_end: usize,
pub is_primary: bool,
pub text: Vec<Text>,
pub label: Option<String>,
pub suggested_replacement: Option<String>,
pub suggestion_applicability: Option<String>,
pub expansion: Option<Box<Expansion>>,
}
impl Span {
pub fn line(&self) -> (usize, usize) {
(self.line_start, self.column_start)
}
pub fn file_name_string(&self) -> String {
self.file_name.to_string_lossy().into_owned()
}
pub fn len(&self) -> usize {
self.byte_end - self.byte_start
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn replace_in_file(self) -> Result<()> {
if let Some(replacement) = self.suggested_replacement {
let mut buffer = fs::read(&self.file_name)?;
let mut end = buffer.split_off(self.byte_end);
buffer.truncate(self.byte_start);
buffer.extend_from_slice(replacement.as_bytes());
buffer.append(&mut end);
fs::write(&self.file_name, buffer)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[allow(missing_docs)]
pub struct Text {
pub text: String,
pub highlight_start: usize,
pub highlight_end: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[allow(missing_docs)]
pub struct Expansion {
pub span: Span,
pub macro_decl_name: String,
pub def_site_span: Option<Span>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(missing_docs)]
pub enum CrateType {
Lib,
Bin,
Rlib,
ProcMacro,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[allow(missing_docs)]
pub struct Profile {
pub opt_level: String,
pub debuginfo: u8,
pub debug_assertions: bool,
pub overflow_checks: bool,
pub test: bool,
}