use std::borrow::Cow;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex, PoisonError};
use skif_config::{Config, Level};
use skif_core::{FileSystem, OsFs, Report, RuleMeta};
use skif_lint::{Executor, Registry, RuleProvider};
use skif_workspace::Workspace;
use crate::cache;
use crate::error::Error;
use crate::targets::Target;
#[must_use]
pub fn builtin_providers(config: &Config, fs: Arc<dyn FileSystem>) -> Vec<Box<dyn RuleProvider>> {
#[cfg(any(feature = "manifest", feature = "js-analysis"))]
let mut providers: Vec<Box<dyn RuleProvider>> = Vec::new();
#[cfg(not(any(feature = "manifest", feature = "js-analysis")))]
let providers: Vec<Box<dyn RuleProvider>> = Vec::new();
#[cfg(feature = "manifest")]
providers.extend(skif_rules::providers());
#[cfg(feature = "js-analysis")]
providers.push(Box::new(crate::js::provider(config, fs)));
#[cfg(not(feature = "js-analysis"))]
let _ = (config, fs);
providers
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct RunOptions<'a> {
cancel: Option<&'a AtomicBool>,
threads: Option<NonZeroUsize>,
executor: Option<&'a Executor>,
rules: Option<&'a [String]>,
config_path: Option<&'a Path>,
extra_entries: &'a [PathBuf],
}
impl<'a> RunOptions<'a> {
#[must_use]
pub const fn with_cancel(mut self, cancel: &'a AtomicBool) -> Self {
self.cancel = Some(cancel);
self
}
#[must_use]
pub const fn with_threads(mut self, threads: NonZeroUsize) -> Self {
self.threads = Some(threads);
self
}
#[must_use]
pub const fn with_executor(mut self, executor: &'a Executor) -> Self {
self.executor = Some(executor);
self
}
#[must_use]
pub const fn with_config_path(mut self, config_path: &'a Path) -> Self {
self.config_path = Some(config_path);
self
}
#[must_use]
pub const fn with_extra_entries(mut self, files: &'a [PathBuf]) -> Self {
self.extra_entries = files;
self
}
#[must_use]
pub const fn with_rules(mut self, rules: &'a [String]) -> Self {
self.rules = Some(rules);
self
}
}
pub struct Linter {
registry: Registry,
config: Config,
cache: Option<Arc<SharedCache>>,
in_flight: Mutex<usize>,
}
type SharedCache = skif_core::CachedFs<cache::Through<Arc<dyn FileSystem>>>;
struct RunGuard<'a> {
cache: Option<&'a SharedCache>,
in_flight: &'a Mutex<usize>,
}
impl<'a> RunGuard<'a> {
fn enter(cache: Option<&'a SharedCache>, in_flight: &'a Mutex<usize>) -> Self {
let mut running = in_flight.lock().unwrap_or_else(PoisonError::into_inner);
if *running == 0
&& let Some(cache) = cache
{
cache.clear();
}
*running += 1;
Self { cache, in_flight }
}
}
impl Drop for RunGuard<'_> {
fn drop(&mut self) {
let mut running = self.in_flight.lock().unwrap_or_else(PoisonError::into_inner);
*running = running.saturating_sub(1);
if *running == 0
&& let Some(cache) = self.cache
{
cache.clear();
}
}
}
impl Linter {
pub fn new(config: &Config) -> Result<Self, Error> {
Self::with_fs(config, Arc::new(OsFs))
}
pub fn with_fs(config: &Config, fs: Arc<dyn FileSystem>) -> Result<Self, Error> {
let cache = cache::shared(fs);
let providers = builtin_providers(config, Arc::clone(&cache) as Arc<dyn FileSystem>);
let mut linter = Self::with_providers(config, providers)?;
linter.cache = Some(cache);
Ok(linter)
}
#[cfg(feature = "js-analysis")]
pub fn with_module_cache(
config: &Config,
fs: Arc<dyn FileSystem>,
modules: Arc<dyn skif_js::ModuleCache>,
) -> Result<Self, Error> {
Self::with_js_provider(config, fs, |provider| provider.with_module_cache(modules))
}
#[cfg(feature = "js-analysis")]
pub fn with_shared_modules(
config: &Config,
fs: Arc<dyn FileSystem>,
modules: &skif_js::SharedModules,
) -> Result<Self, Error> {
Self::with_js_provider(config, fs, |provider| provider.with_shared_modules(modules.clone()))
}
#[cfg(feature = "js-analysis")]
#[must_use]
pub fn shared_modules(
targets: &[Target],
host: Option<Arc<dyn skif_js::ModuleCache>>,
) -> Option<skif_js::SharedModules> {
let roots = targets.iter().map(|target| target.workspace.root.clone()).collect();
skif_js::SharedModules::for_run(roots, host)
}
pub fn for_targets(targets: &[Target], fs: Arc<dyn FileSystem>) -> Result<Vec<Self>, Error> {
#[cfg(feature = "js-analysis")]
{
Self::for_run(targets, fs, None)
}
#[cfg(not(feature = "js-analysis"))]
{
targets
.iter()
.map(|target| Self::with_fs(&target.config, Arc::clone(&fs)).map_err(|error| target.locate(error)))
.collect()
}
}
#[cfg(feature = "js-analysis")]
pub fn for_targets_with_cache(
targets: &[Target],
fs: Arc<dyn FileSystem>,
modules: Arc<dyn skif_js::ModuleCache>,
) -> Result<Vec<Self>, Error> {
Self::for_run(targets, fs, Some(modules))
}
#[cfg(feature = "js-analysis")]
fn for_run(
targets: &[Target],
fs: Arc<dyn FileSystem>,
host: Option<Arc<dyn skif_js::ModuleCache>>,
) -> Result<Vec<Self>, Error> {
let modules = Self::shared_modules(targets, host);
targets
.iter()
.zip(std::iter::repeat_n(fs, targets.len()))
.map(|(target, fs)| {
let linter = match &modules {
Some(modules) => Self::with_shared_modules(&target.config, fs, modules),
None => Self::with_fs(&target.config, fs),
};
linter.map_err(|error| target.locate(error))
})
.collect()
}
#[cfg(feature = "js-analysis")]
fn with_js_provider(
config: &Config,
fs: Arc<dyn FileSystem>,
modules: impl FnOnce(skif_js::JsProvider) -> skif_js::JsProvider,
) -> Result<Self, Error> {
let cache = cache::shared(fs);
let shared = Arc::clone(&cache) as Arc<dyn FileSystem>;
#[cfg(feature = "manifest")]
let mut providers: Vec<Box<dyn RuleProvider>> = skif_rules::providers();
#[cfg(not(feature = "manifest"))]
let mut providers: Vec<Box<dyn RuleProvider>> = Vec::new();
providers.push(Box::new(modules(crate::js::provider(config, shared))));
let mut linter = Self::with_providers(config, providers)?;
linter.cache = Some(cache);
Ok(linter)
}
pub fn with_providers(config: &Config, providers: Vec<Box<dyn RuleProvider>>) -> Result<Self, Error> {
let registry = Registry::new(providers)?;
registry.validate_config(config)?;
Ok(Self {
registry,
config: config.clone(),
cache: None,
in_flight: Mutex::new(0),
})
}
#[cfg(feature = "manifest")]
#[must_use]
pub fn instances(&self, workspace: &Workspace, report: &Report) -> Vec<skif_rules::policy::InstanceStatus> {
let enabled = self.registry.enabled(&self.config);
skif_rules::policy::inspect(workspace, &self.config, &|id| enabled.contains(id), &report.diagnostics)
}
pub fn rules(&self) -> impl Iterator<Item = &'static RuleMeta> + '_ {
self.registry.rules()
}
#[must_use]
pub const fn config(&self) -> &Config {
&self.config
}
pub fn discover_with_fs(
&self,
root: &Path,
fs: &dyn FileSystem,
options: &RunOptions<'_>,
) -> Result<Workspace, Error> {
Ok(Workspace::discover_with_threads(
root,
&self.config,
fs,
options.threads,
)?)
}
pub fn run(&self, workspace: &Workspace) -> Result<Report, Error> {
self.run_with(workspace, &RunOptions::default())
}
pub fn run_with(&self, workspace: &Workspace, options: &RunOptions<'_>) -> Result<Report, Error> {
let config = self.restricted(options.rules)?;
let _guard = RunGuard::enter(self.cache.as_deref(), &self.in_flight);
let sequential = options.threads.is_some_and(|threads| threads.get() == 1);
let built = match (options.executor, options.threads) {
(None, Some(threads)) if !sequential => {
Some(Executor::new(threads.get()).map_err(|error| Error::ThreadPool(error.to_string()))?)
}
_ => None,
};
let mut run_options = skif_lint::RunOptions::default().with_sequential(sequential);
if let Some(executor) = options.executor.or(built.as_ref()) {
run_options = run_options.with_executor(executor);
}
if let Some(cancel) = options.cancel {
run_options = run_options.with_cancel(cancel);
}
if let Some(path) = options.config_path {
run_options = run_options.with_config_path(path);
}
run_options = run_options.with_extra_entries(options.extra_entries);
if let Some(cache) = self.cache.as_deref() {
run_options = run_options.with_fs(cache);
}
Ok(skif_lint::run(&self.registry, workspace, &config, &run_options)?)
}
fn restricted(&self, selected: Option<&[String]>) -> Result<Cow<'_, Config>, Error> {
let Some(selected) = selected else {
return Ok(Cow::Borrowed(&self.config));
};
let mut config = self.config.clone();
if let Some(unknown) = selected.iter().find(|id| self.registry.get(id).is_none()) {
return Err(Error::UnknownRule(unknown.clone()));
}
for meta in self.registry.rules() {
if !selected.iter().any(|id| id == meta.id) {
config.rules.entry(meta.id.to_owned()).or_default().severity = Some(Level::Off);
config.cli_levels.insert(meta.id.to_owned(), Level::Off);
}
}
Ok(Cow::Owned(config))
}
}
#[cfg(test)]
mod tests {
use std::io;
use std::sync::atomic::{AtomicUsize, Ordering};
use skif_core::{DirEntry, MemoryFs};
use super::*;
struct Counting {
inner: MemoryFs,
reads: Arc<AtomicUsize>,
}
impl FileSystem for Counting {
fn read_to_string(&self, path: &Path) -> io::Result<Arc<str>> {
self.reads.fetch_add(1, Ordering::SeqCst);
self.inner.read_to_string(path)
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
self.inner.read_dir(path)
}
fn is_file(&self, path: &Path) -> bool {
self.inner.is_file(path)
}
fn is_dir(&self, path: &Path) -> bool {
self.inner.is_dir(path)
}
}
fn counting_cache() -> (Arc<SharedCache>, Arc<AtomicUsize>) {
let reads = Arc::new(AtomicUsize::new(0));
let fs = Counting {
inner: MemoryFs::new().with_file("/r/package.json", "{}"),
reads: Arc::clone(&reads),
};
(cache::shared(Arc::new(fs)), reads)
}
fn read(cache: &SharedCache) {
cache.read_to_string(Path::new("/r/package.json")).unwrap();
}
#[test]
fn overlapping_runs_share_the_cache_and_the_last_one_empties_it() {
let (cache, reads) = counting_cache();
let in_flight = Mutex::new(0);
let first = RunGuard::enter(Some(&cache), &in_flight);
read(&cache);
let second = RunGuard::enter(Some(&cache), &in_flight);
read(&cache);
assert_eq!(
reads.load(Ordering::SeqCst),
1,
"the second run must not empty the cache"
);
drop(first);
read(&cache);
assert_eq!(
reads.load(Ordering::SeqCst),
1,
"a run ending while another runs keeps the cache"
);
drop(second);
read(&cache);
assert_eq!(reads.load(Ordering::SeqCst), 2, "the last run ending empties the cache");
}
#[test]
fn a_run_that_starts_on_an_idle_linter_begins_with_an_empty_cache() {
let (cache, reads) = counting_cache();
let in_flight = Mutex::new(0);
read(&cache);
let _run = RunGuard::enter(Some(&cache), &in_flight);
read(&cache);
assert_eq!(reads.load(Ordering::SeqCst), 2);
}
}