use crate::adapters::{PulldownMarkdownParser, StdFileSystemProvider};
use crate::analyzer::SkillDocument;
use crate::artifact_graph::ArtifactGraph;
use crate::findings::ArtifactKind;
use crate::ports::{FileSystemProvider, MarkdownParser};
use crate::rules::RuleEngine;
use crate::scanner_support::{load_optional_baseline, load_optional_policy, load_optional_waivers};
pub use crate::scanner_types::{ScanError, ScanOptions, ScanResult, ScanTargetMode};
use crate::services::{ArtifactAnalysisService, FileDiscoveryService, ScanFilterService};
use crate::{scanner_execution, scanner_graph};
use std::path::Path;
pub struct Scanner<
F: FileSystemProvider = StdFileSystemProvider,
P: MarkdownParser = PulldownMarkdownParser,
> {
pub(crate) engine: RuleEngine,
pub(crate) artifact_analysis: ArtifactAnalysisService,
pub(crate) file_discovery: FileDiscoveryService<F>,
pub(crate) filter_service: ScanFilterService,
pub(crate) parser: P,
}
impl Scanner<StdFileSystemProvider, PulldownMarkdownParser> {
#[must_use = "Scanner::new() returns a Result that should be used"]
pub fn new() -> Result<Self, ScanError> {
Self::with_std_adapters(ScanOptions::default())
}
#[must_use = "Scanner::with_std_adapters() returns a Result that should be used"]
pub fn with_std_adapters(options: ScanOptions) -> Result<Self, ScanError> {
let mut engine = RuleEngine::with_defaults()?;
if let Some(ref rules_dir) = options.rules_dir {
engine.load_from_dir(rules_dir)?;
}
let baseline = load_optional_baseline(options.baseline_path.as_deref())?;
let waivers = load_optional_waivers(options.waivers_path.as_deref())?;
let policy = load_optional_policy(options.policy_path.as_deref())?;
Ok(Self {
engine,
artifact_analysis: ArtifactAnalysisService::new(),
file_discovery: FileDiscoveryService::new(options.recursive),
filter_service: ScanFilterService::with_policy_state(
options, baseline, waivers, policy,
),
parser: PulldownMarkdownParser::new(),
})
}
}
impl<F: FileSystemProvider, P: MarkdownParser> Scanner<F, P> {
#[must_use = "Scanner::with_custom_adapters() returns a Result that should be used"]
pub fn with_custom_adapters(
options: ScanOptions,
fs_provider: F,
parser: P,
) -> Result<Self, ScanError> {
let mut engine = RuleEngine::with_defaults()?;
if let Some(ref rules_dir) = options.rules_dir {
engine.load_from_dir(rules_dir)?;
}
let baseline = load_optional_baseline(options.baseline_path.as_deref())?;
let waivers = load_optional_waivers(options.waivers_path.as_deref())?;
let policy = load_optional_policy(options.policy_path.as_deref())?;
Ok(Self {
engine,
artifact_analysis: ArtifactAnalysisService::new(),
file_discovery: FileDiscoveryService::with_fs_provider(options.recursive, fs_provider),
filter_service: ScanFilterService::with_policy_state(
options, baseline, waivers, policy,
),
parser,
})
}
pub(crate) fn build_artifact_graph(&self, doc: &SkillDocument) -> ArtifactGraph {
scanner_graph::build_artifact_graph::<F>(&self.artifact_analysis, doc)
}
pub(crate) fn artifact_kind_for_path(path: &Path) -> ArtifactKind {
scanner_graph::artifact_kind_for_path::<F>(path)
}
pub(crate) fn sibling_files(path: &Path) -> Vec<std::path::PathBuf> {
scanner_graph::sibling_files(path)
}
pub(crate) fn derive_package_id(path: &Path) -> Option<String> {
scanner_graph::derive_package_id(path)
}
pub fn scan_file(&self, path: impl AsRef<Path>) -> Result<ScanResult, ScanError> {
let path = path.as_ref();
if !path.exists() {
return Err(ScanError::PathNotFound(path.to_path_buf()));
}
scanner_execution::scan_document_path(self, path)
}
pub fn scan_skill_file(&self, path: impl AsRef<Path>) -> Result<ScanResult, ScanError> {
let path = path.as_ref();
if !path.exists() {
return Err(ScanError::PathNotFound(path.to_path_buf()));
}
if !FileDiscoveryService::<F>::is_explicit_skill_file(path) {
return Err(ScanError::InvalidSkillEntrypoint(path.to_path_buf()));
}
scanner_execution::scan_document_path(self, path)
}
pub fn scan_package(&self, path: impl AsRef<Path>) -> Result<Vec<ScanResult>, ScanError> {
let path = path.as_ref();
if !path.exists() {
return Err(ScanError::PathNotFound(path.to_path_buf()));
}
if path.is_file() {
return Ok(vec![self.scan_skill_file(path)?]);
}
let targets = scanner_execution::discover_package_targets(self, path)?;
let mut results = Vec::new();
for target in targets {
match self.scan_file(&target) {
Ok(result) => results.push(result),
Err(err) => tracing::warn!("Failed to scan {}: {}", target.display(), err),
}
}
Ok(results)
}
pub fn scan_dir(&self, path: impl AsRef<Path>) -> Result<Vec<ScanResult>, ScanError> {
let path = path.as_ref();
if !path.exists() {
return Err(ScanError::PathNotFound(path.to_path_buf()));
}
let skill_files = self.file_discovery.discover_skills(path);
let mut results = Vec::new();
for file_path in skill_files {
match self.scan_file(&file_path) {
Ok(result) => results.push(result),
Err(err) => tracing::warn!("Failed to scan {}: {}", file_path.display(), err),
}
}
Ok(results)
}
pub fn scan(&self, path: impl AsRef<Path>) -> Result<Vec<ScanResult>, ScanError> {
let path = path.as_ref();
match self.filter_service.target_mode() {
ScanTargetMode::Auto => {
if path.is_file() {
Ok(vec![self.scan_file(path)?])
} else if path.is_dir() {
self.scan_dir(path)
} else {
Err(ScanError::PathNotFound(path.to_path_buf()))
}
}
ScanTargetMode::File => Ok(vec![self.scan_skill_file(path)?]),
ScanTargetMode::Package => self.scan_package(path),
}
}
pub fn rule_count(&self) -> usize {
self.engine.rule_count()
}
pub fn rules(&self) -> Vec<&crate::rules::Rule> {
self.engine.rules()
}
}
#[path = "scanner_tests.rs"]
#[cfg(test)]
mod scanner_tests;