use std::collections::BTreeSet;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use serde_json::{Map, Value};
use skif::render::{RenderOptions, render_github, render_human, render_sarif};
use skif::{FileSystem, Report, RuleMeta};
use skif_report::{ErrorInfo, escape_controls, escape_multiline, render_json_error_info_with, render_json_with};
use crate::args::Format;
use crate::error::{CliError, Kind};
use crate::fix::{self, FixRun, RootFix};
use crate::roots;
use crate::write::write_atomic;
pub fn write_stdout(bytes: &[u8]) -> Result<(), CliError> {
let mut out = io::stdout().lock();
match out.write_all(bytes).and_then(|()| out.flush()) {
Err(error) if error.kind() != io::ErrorKind::BrokenPipe => {
Err(CliError::new(Kind::Io, format!("cannot write to stdout: {error}")))
}
_ => Ok(()),
}
}
pub fn stderr_line(text: &str) {
let mut err = io::stderr().lock();
let _ = err.write_all(text.as_bytes()).and_then(|()| err.write_all(b"\n"));
}
pub fn emit_error(error: &CliError, format: Format) {
emit_error_with(error, format, Map::new());
}
pub fn emit_error_with(error: &CliError, format: Format, extra: Map<String, serde_json::Value>) {
if format == Format::Json {
let mut info = ErrorInfo::new(error.kind.as_str(), &error.message);
if let Some(path) = &error.path {
info = info.at(path, error.line, error.column);
}
let mut buffer = Vec::new();
if render_json_error_info_with(&info, extra, &mut buffer).is_ok() && write_stdout(&buffer).is_ok() {
return;
}
}
stderr_line(&format!("error: {}", escape_multiline(&error.to_string())));
}
fn framed_files(report: &Report) -> BTreeSet<PathBuf> {
let mut files = BTreeSet::new();
for diagnostic in &report.diagnostics {
if diagnostic.location.span.is_some() {
files.insert(diagnostic.location.path.clone());
}
for related in &diagnostic.related {
if related.location.span.is_some() {
files.insert(related.location.path.clone());
}
}
}
files
}
pub struct Rendering<'a> {
pub format: Format,
pub annotations: bool,
pub color: bool,
pub root: &'a Path,
pub repo_root: Option<PathBuf>,
pub rules: Vec<&'static RuleMeta>,
pub multi: bool,
pub errors: Vec<Value>,
pub truncated: Value,
}
pub struct RootView<'a> {
pub prefix: &'a Path,
pub report: &'a Report,
pub json: Option<Value>,
pub fix: Option<&'a FixRun>,
}
pub fn render(
report: &Report,
roots: &[RootView<'_>],
rendering: &Rendering<'_>,
fs: &dyn FileSystem,
fixes: &[RootFix<'_>],
) -> Result<Vec<u8>, CliError> {
let root = rendering.root;
let mut options = RenderOptions::default()
.with_color(rendering.color)
.with_root(root)
.with_tool_version(env!("CARGO_PKG_VERSION"));
if let Some(repo_root) = &rendering.repo_root {
options = options.with_repo_root(repo_root);
}
if matches!(rendering.format, Format::Human | Format::Sarif) {
for path in framed_files(report) {
if let Ok(text) = fs.read_to_string(&root.join(&path)) {
options = options.with_source(&path, text.to_string());
}
}
}
if rendering.format == Format::Sarif {
options = options.with_rules(rendering.rules.iter().copied());
}
let mut buffer = Vec::new();
write_report(report, roots, rendering, &options, fixes, &mut buffer)
.map_err(|error| CliError::new(Kind::Io, format!("cannot render the report: {error}")))?;
Ok(buffer)
}
fn write_human_roots(
roots: &[RootView<'_>],
rendering: &Rendering<'_>,
options: &RenderOptions,
out: &mut Vec<u8>,
) -> io::Result<()> {
for view in roots {
if rendering.multi {
writeln!(out, "==> {}", escape_controls(&roots::display(view.prefix)))?;
}
if let Some(run) = view.fix {
run.write_human_at(view.prefix, out)?;
}
render_human(view.report, options, out)?;
if rendering.multi {
writeln!(out)?;
}
}
Ok(())
}
fn write_report(
report: &Report,
roots: &[RootView<'_>],
rendering: &Rendering<'_>,
options: &RenderOptions,
fixes: &[RootFix<'_>],
out: &mut Vec<u8>,
) -> io::Result<()> {
match rendering.format {
Format::Human => {
write_human_roots(roots, rendering, options, out)?;
if rendering.multi {
writeln!(out, "Linted {}.", plural_roots(roots.len()))?;
}
if rendering.annotations {
render_github(report, options, out)?;
}
Ok(())
}
Format::Json => {
let mut extra = Map::new();
if !fixes.is_empty() {
extra.insert("fix".to_owned(), fix::fixes_json(fixes));
}
if rendering.multi {
extra.insert(
"roots".to_owned(),
Value::Array(roots.iter().filter_map(|view| view.json.clone()).collect()),
);
if !rendering.errors.is_empty() {
extra.insert("errors".to_owned(), Value::Array(rendering.errors.clone()));
}
if rendering.truncated.as_array().is_some_and(|dirs| !dirs.is_empty()) {
extra.insert("truncated".to_owned(), rendering.truncated.clone());
}
}
render_json_with(report, options, extra, out)
}
Format::Sarif => render_sarif(report, options, out),
Format::Github => render_github(report, options, out),
}
}
fn plural_roots(count: usize) -> String {
if count == 1 {
"1 root".to_owned()
} else {
format!("{count} roots")
}
}
pub fn emit(bytes: &[u8], output: Option<&Path>) -> Result<(), CliError> {
match output {
Some(path) => write_atomic(path, bytes).map_err(|error| CliError::io("cannot write", path, &error)),
None => write_stdout(bytes),
}
}