use anyhow::Result;
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::processor::{Processor, ProcessorType};
pub struct ProcessorPlugin {
pub name: &'static str,
pub processor_type: ProcessorType,
pub version: u32,
pub create: fn(&toml::Value) -> Result<Box<dyn Processor>>,
pub fields: &'static [crate::config::FieldSpec],
pub omit_standard_fields: &'static [&'static str],
pub scan_defaults: Option<crate::config::ScanDefaultsData>,
pub defaults: Option<crate::config::ProcessorDefaults>,
pub defconfig_json: fn(&str) -> Option<String>,
pub keywords: &'static [&'static str],
pub description: &'static str,
pub is_native: bool,
pub is_rust: bool,
pub can_fix: bool,
pub supports_batch: bool,
pub max_jobs_cap: Option<usize>,
}
unsafe impl Sync for ProcessorPlugin {}
inventory::collect!(ProcessorPlugin);
pub fn all_plugins() -> impl Iterator<Item = &'static ProcessorPlugin> {
inventory::iter::<ProcessorPlugin>.into_iter()
}
pub const NAME_PREFIX: &str = "processor.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParsedName<'a> {
pub processor_type: ProcessorType,
pub short: &'a str,
pub instance: Option<&'a str>,
}
impl ParsedName<'_> {
pub fn pname(&self) -> String {
format!(
"{NAME_PREFIX}{}.{}",
self.processor_type.as_str(),
self.short
)
}
}
pub fn parse_name(name: &str) -> Option<ParsedName<'_>> {
let rest = name.strip_prefix(NAME_PREFIX)?;
let (type_str, rest) = rest.split_once('.')?;
let processor_type = ProcessorType::parse(type_str)?;
let (short, instance) = match rest.split_once('.') {
Some((short, instance)) => (short, Some(instance)),
None => (rest, None),
};
if short.is_empty() || instance.is_some_and(str::is_empty) {
return None;
}
Some(ParsedName {
processor_type,
short,
instance,
})
}
pub fn pname_of(name: &str) -> String {
parse_name(name).map_or_else(|| name.to_string(), |p| p.pname())
}
impl ProcessorPlugin {
pub fn pname(&self) -> String {
format!(
"{NAME_PREFIX}{}.{}",
self.processor_type.as_str(),
self.name
)
}
}
pub fn find_plugin(name: &str) -> Option<&'static ProcessorPlugin> {
let ParsedName {
processor_type,
short,
..
} = parse_name(name)?;
all_plugins().find(|p| p.processor_type == processor_type && p.name == short)
}
pub fn description_of(name: &str) -> &'static str {
find_plugin(name).map_or("", |p| p.description)
}
pub fn processor_type_of(name: &str) -> crate::processor::ProcessorType {
parse_name(name).map_or(crate::processor::ProcessorType::Checker, |p| {
p.processor_type
})
}
pub fn is_native(name: &str) -> bool {
find_plugin(name).is_some_and(|p| p.is_native)
}
pub fn is_rust(name: &str) -> bool {
find_plugin(name).is_some_and(|p| p.is_rust)
}
pub fn can_fix(name: &str) -> bool {
find_plugin(name).is_some_and(|p| p.can_fix)
}
pub fn processor_version(name: &str) -> Option<u32> {
find_plugin(name).map(|p| p.version)
}
pub fn all_pnames() -> Vec<String> {
let mut names: Vec<String> = all_plugins().map(ProcessorPlugin::pname).collect();
names.sort_unstable();
names
}
pub fn processor_name_parser() -> clap::builder::PossibleValuesParser {
let names: Vec<&'static str> = all_pnames()
.into_iter()
.map(|n| &*Box::leak(n.into_boxed_str()))
.collect();
clap::builder::PossibleValuesParser::new(names)
}
pub fn apply_all_defaults(
name: &str,
value: &mut toml::Value,
provenance: &mut crate::config::ProvenanceMap,
) {
crate::config::apply_processor_defaults(name, value, provenance);
crate::config::apply_scan_defaults(name, value, provenance);
}
pub fn deserialize_and_create<C: Default + DeserializeOwned>(
config_toml: &toml::Value,
ctor: fn(C) -> Box<dyn Processor>,
) -> Result<Box<dyn Processor>> {
let cfg: C = toml::from_str(&toml::to_string(config_toml)?)?;
Ok(ctor(cfg))
}
pub fn deserialize_and_try_create<C: Default + DeserializeOwned>(
config_toml: &toml::Value,
ctor: fn(C) -> Result<Box<dyn Processor>>,
) -> Result<Box<dyn Processor>> {
let cfg: C = toml::from_str(&toml::to_string(config_toml)?)?;
ctor(cfg)
}
pub fn default_config_json<C: Default + DeserializeOwned + Serialize>(
name: &str,
) -> Option<String> {
let mut val = toml::Value::Table(toml::map::Map::new());
let mut prov = crate::config::ProvenanceMap::new();
apply_all_defaults(name, &mut val, &mut prov);
let cfg: C = toml::from_str(&toml::to_string(&val).ok()?).ok()?;
let json_val = serde_json::to_value(&cfg).ok()?;
if cfg!(debug_assertions)
&& let Some(obj) = json_val.as_object()
{
use crate::config::KnownFields as _;
let known: std::collections::HashSet<&str> =
crate::config::ProcessorConfig::known_fields_for(name)
.unwrap_or_default()
.into_iter()
.chain(
crate::config::StandardConfig::known_fields()
.iter()
.copied(),
)
.chain(crate::config::SCAN_CONFIG_FIELDS.iter().copied())
.chain(crate::config::STANDARD_EXTRA_FIELDS.iter().copied())
.collect();
for key in obj.keys() {
debug_assert!(
known.contains(key.as_str()),
"Processor '{name}': default config field '{key}' is serialized but not declared in its FieldSpec list or scan fields"
);
}
}
serde_json::to_string_pretty(&json_val).ok()
}
pub fn typed_known_fields<C: crate::config::KnownFields>() -> &'static [&'static str] {
C::known_fields()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accessors_resolve_instance_names_like_type_names() {
for plugin in all_plugins() {
let pname = plugin.pname();
let instance = format!("{pname}.someinst");
assert_eq!(
processor_version(&instance),
Some(plugin.version),
"processor_version must strip the instance suffix for '{instance}'"
);
assert_eq!(processor_version(&pname), Some(plugin.version));
assert_eq!(
is_native(&instance),
is_native(&pname),
"is_native must strip the instance suffix for '{instance}'"
);
assert_eq!(
is_rust(&instance),
is_rust(&pname),
"is_rust must strip the instance suffix for '{instance}'"
);
assert_eq!(
can_fix(&instance),
can_fix(&pname),
"can_fix must strip the instance suffix for '{instance}'"
);
assert_eq!(
description_of(&instance),
description_of(&pname),
"description_of must strip the instance suffix for '{instance}'"
);
assert_eq!(
processor_type_of(&instance),
plugin.processor_type,
"the type is a segment of the name: '{instance}'"
);
}
}
#[test]
fn parse_name_decision_table() {
let p = parse_name("processor.checker.ruff").unwrap();
assert_eq!(p.processor_type, ProcessorType::Checker);
assert_eq!(p.short, "ruff");
assert_eq!(p.instance, None);
assert_eq!(p.pname(), "processor.checker.ruff");
let p = parse_name("processor.checker.script.lint_a").unwrap();
assert_eq!(p.instance, Some("lint_a"));
assert_eq!(p.pname(), "processor.checker.script");
assert_eq!(
pname_of("processor.checker.script.lint_a"),
"processor.checker.script"
);
let p = parse_name("processor.lua.myplugin").unwrap();
assert_eq!(p.processor_type, ProcessorType::Lua);
assert!(find_plugin("processor.lua.myplugin").is_none());
for bad in [
"ruff",
"checker.ruff",
"processor.ruff",
"processor.nosuchtype.ruff",
"processor.checker.",
"processor.checker.ruff.",
"processor..ruff",
] {
assert!(parse_name(bad).is_none(), "{bad} must not parse");
}
}
#[test]
fn short_names_are_scoped_by_type() {
let creator = find_plugin("processor.creator.generic").unwrap();
let explicit = find_plugin("processor.explicit.generic").unwrap();
assert_eq!(creator.processor_type, ProcessorType::Creator);
assert_eq!(explicit.processor_type, ProcessorType::Explicit);
assert!(find_plugin("processor.checker.generic").is_none());
}
#[test]
fn native_processors_are_rust() {
for plugin in all_plugins() {
if plugin.is_native {
assert!(
plugin.is_rust,
"Processor '{}' is native (pure Rust) but declares is_rust: false",
plugin.name
);
}
}
}
}