use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::{Component, Path, PathBuf};
use rayon::prelude::*;
use serde_json::{Value, json};
use skif::{
ConfigOverrides, Diagnostic, FileSystem, Linter, Location, Report, RuleMeta, RunOptions, Target, Workspace, slash,
};
use crate::error::{CliError, Kind};
pub struct RootError {
pub prefix: PathBuf,
pub error: CliError,
}
impl RootError {
pub fn describe(&self) -> String {
format!("{}: {}", display(&self.prefix), self.error.message)
}
pub fn to_json(&self) -> Value {
json!({
"path": display(&self.prefix),
"kind": self.error.kind.as_str(),
"message": self.error.message,
})
}
}
pub struct Resolved {
pub base: PathBuf,
pub targets: Vec<Target>,
pub multi: bool,
pub failures: Vec<RootError>,
pub truncated: Vec<PathBuf>,
}
fn directory_of(start: &Path, fs: &dyn FileSystem) -> PathBuf {
if fs.is_dir(start) {
start.to_path_buf()
} else {
start.parent().unwrap_or(start).to_path_buf()
}
}
fn prefix_of(base: &Path, root: &Path) -> PathBuf {
root.strip_prefix(base).unwrap_or(root).to_path_buf()
}
pub fn resolve(
start: &Path,
recursive: bool,
fs: &dyn FileSystem,
overrides: &ConfigOverrides,
) -> Result<Resolved, CliError> {
let found = if recursive {
None
} else {
match skif::find_target(start, fs, overrides) {
Ok(target) => Some(target),
Err(skif::Error::NoWorkspace(_)) => None,
Err(error) => return Err(error.into()),
}
};
if let Some(target) = found {
return Ok(Resolved {
base: target.root.clone(),
targets: vec![target],
multi: false,
failures: Vec::new(),
truncated: Vec::new(),
});
}
let base = directory_of(start, fs);
let discovery = skif::discover_targets(start, fs, overrides)?;
let mut failures = discovery.failures.into_iter();
if discovery.targets.is_empty() {
return Err(match failures.next() {
Some(failure) => {
let mut error = CliError::from(failure.error);
error.message = format!("{}: {}", display(&prefix_of(&base, &failure.root)), error.message);
error
}
None => CliError::new(
Kind::Workspace,
format!("no workspace or package found at or below {}", start.display()),
),
});
}
Ok(Resolved {
failures: failures
.map(|failure| RootError {
prefix: prefix_of(&base, &failure.root),
error: failure.error.into(),
})
.collect(),
truncated: discovery.truncated.iter().map(|dir| prefix_of(&base, dir)).collect(),
base,
targets: discovery.targets,
multi: true,
})
}
pub struct Linters {
linters: Vec<Linter>,
of_target: Vec<usize>,
}
impl Linters {
pub fn build(targets: &[Target]) -> Result<Self, CliError> {
let mut linters: Vec<Linter> = Vec::new();
let mut of_target = Vec::with_capacity(targets.len());
for target in targets {
let index = if let Some(index) = linters.iter().position(|linter| linter.config() == &target.config) {
index
} else {
linters.push(Linter::new(&target.config)?);
linters.len() - 1
};
of_target.push(index);
}
Ok(Self { linters, of_target })
}
pub fn get(&self, index: usize) -> &Linter {
&self.linters[index]
}
pub fn rules(&self) -> Vec<&'static RuleMeta> {
let mut rules: BTreeMap<&'static str, &'static RuleMeta> = BTreeMap::new();
for meta in self.linters.iter().flat_map(Linter::rules) {
rules.entry(meta.id).or_insert(meta);
}
rules.into_values().collect()
}
}
pub struct RootRun {
pub prefix: PathBuf,
pub config: skif::Config,
pub config_path: Option<PathBuf>,
pub linter: usize,
pub workspace: Workspace,
pub report: Report,
}
pub fn lint_loaded(linter: &Linter, workspace: &Workspace, config_path: Option<&Path>) -> Result<Report, CliError> {
let mut options = RunOptions::default();
if let Some(path) = config_path {
options = options.with_config_path(path);
}
Ok(linter.run_with(workspace, &options)?)
}
pub fn lint_targets(base: &Path, targets: Vec<Target>, linters: &Linters) -> Result<Vec<RootRun>, CliError> {
let reports: Vec<Result<Report, CliError>> = targets
.par_iter()
.zip(&linters.of_target)
.map(|(target, &linter)| lint_loaded(linters.get(linter), &target.workspace, target.config_path().as_deref()))
.collect();
targets
.into_iter()
.zip(reports)
.zip(&linters.of_target)
.map(|((target, report), &linter)| {
Ok(RootRun {
prefix: prefix_of(base, &target.root),
config_path: target.config_path(),
config: target.config,
linter,
workspace: target.workspace,
report: report?,
})
})
.collect()
}
fn join(prefix: &Path, path: &Path) -> PathBuf {
let mut joined = PathBuf::new();
for component in prefix.components().chain(path.components()) {
match component {
Component::ParentDir if joined.pop() => {}
other => joined.push(other),
}
}
joined
}
fn relocate(location: &mut Location, prefix: &Path) {
location.path = join(prefix, &location.path);
}
pub fn prefixed<'a>(report: &'a Report, prefix: &Path) -> Cow<'a, Report> {
if prefix.as_os_str().is_empty() {
return Cow::Borrowed(report);
}
let diagnostics: Vec<Diagnostic> = report
.diagnostics
.iter()
.cloned()
.map(|mut diagnostic| {
relocate(&mut diagnostic.location, prefix);
for related in &mut diagnostic.related {
relocate(&mut related.location, prefix);
}
diagnostic
})
.collect();
Cow::Owned(Report::new(diagnostics, report.summary.packages))
}
pub fn aggregate(reports: &[Cow<'_, Report>]) -> Report {
Report::new(
reports
.iter()
.flat_map(|report| report.diagnostics.iter().cloned())
.collect(),
reports.iter().map(|report| report.summary.packages).sum(),
)
}
pub fn display(prefix: &Path) -> String {
if prefix.as_os_str().is_empty() {
".".to_owned()
} else {
slash(prefix)
}
}
pub fn root_json(run: &RootRun, report: &Report) -> Value {
let packages: Vec<String> = run
.workspace
.all_packages()
.filter(|package| !run.workspace.is_ignored(package.id))
.map(|package| display(&join(&run.prefix, &package.dir)))
.collect();
json!({
"path": display(&run.prefix),
"packages": packages,
"summary": serde_json::to_value(report.summary).unwrap_or(Value::Null),
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn join_resolves_parent_segments() {
assert_eq!(
join(Path::new("a/b"), Path::new("package.json")),
Path::new("a/b/package.json")
);
assert_eq!(join(Path::new("a"), Path::new("../skif.toml")), Path::new("skif.toml"));
assert_eq!(join(Path::new(""), Path::new("x")), Path::new("x"));
}
#[test]
fn display_names_the_base_as_a_dot() {
assert_eq!(display(Path::new("")), ".");
assert_eq!(display(Path::new("a/b")), "a/b");
}
#[test]
fn a_root_outside_the_base_keeps_its_full_path() {
assert_eq!(prefix_of(Path::new("/a"), Path::new("/a/b/c")), Path::new("b/c"));
assert_eq!(prefix_of(Path::new("/a"), Path::new("/a")), Path::new(""));
assert_eq!(prefix_of(Path::new("/a"), Path::new("/z/b")), Path::new("/z/b"));
}
#[test]
fn prefixing_with_the_base_borrows_and_otherwise_relocates() {
let report = Report::new(Vec::new(), 3);
assert!(matches!(prefixed(&report, Path::new("")), Cow::Borrowed(_)));
assert!(matches!(prefixed(&report, Path::new("a")), Cow::Owned(_)));
let both = [prefixed(&report, Path::new("a")), prefixed(&report, Path::new(""))];
assert_eq!(aggregate(&both).summary.packages, 6);
}
#[test]
fn a_root_error_names_the_root() {
let error = RootError {
prefix: PathBuf::from("pkgs/a"),
error: CliError::new(Kind::Workspace, "boom"),
};
assert_eq!(error.describe(), "pkgs/a: boom");
assert_eq!(error.to_json()["path"], "pkgs/a");
assert_eq!(error.to_json()["kind"], "workspace");
}
}