use crate::ir::DamlModule;
use daml_syntax::{CharColumn, LineNumber};
use serde::{Serialize, Serializer};
use std::error::Error;
use std::path::PathBuf;
fn serialize_line_number<S>(line: &LineNumber, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(line.get() as u64)
}
fn serialize_char_column<S>(column: &CharColumn, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(column.get() as u64)
}
#[derive(Debug)]
#[non_exhaustive]
pub struct DetectError {
detector: String,
message: String,
source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
impl DetectError {
#[must_use]
pub fn new(detector: impl Into<String>, message: impl Into<String>) -> Self {
Self {
detector: detector.into(),
message: message.into(),
source: None,
}
}
#[must_use]
pub fn with_source<E>(
detector: impl Into<String>,
message: impl Into<String>,
source: E,
) -> Self
where
E: Error + Send + Sync + 'static,
{
Self {
detector: detector.into(),
message: message.into(),
source: Some(Box::new(source)),
}
}
#[must_use]
pub fn detector(&self) -> &str {
&self.detector
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn source(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
self.source.as_deref()
}
}
impl std::fmt::Display for DetectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "detector '{}': {}", self.detector, self.message)
}
}
impl std::error::Error for DetectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_deref()
.map(|source| source as &(dyn std::error::Error + 'static))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeverityParseError {
value: String,
}
impl SeverityParseError {
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
impl std::fmt::Display for SeverityParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"invalid severity: {} (expected one of critical|high|medium|low|info)",
self.value
)
}
}
impl std::error::Error for SeverityParseError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub enum Severity {
Critical,
High,
Medium,
Low,
Info,
}
impl Severity {
#[must_use]
pub const fn rank(self) -> u8 {
match self {
Self::Critical => 5,
Self::High => 4,
Self::Medium => 3,
Self::Low => 2,
Self::Info => 1,
}
}
#[must_use]
pub const fn meets_or_exceeds(self, threshold: Self) -> bool {
self.rank() >= threshold.rank()
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Critical => write!(f, "CRITICAL"),
Self::High => write!(f, "HIGH"),
Self::Medium => write!(f, "MEDIUM"),
Self::Low => write!(f, "LOW"),
Self::Info => write!(f, "INFO"),
}
}
}
impl std::str::FromStr for Severity {
type Err = SeverityParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"critical" => Ok(Self::Critical),
"high" => Ok(Self::High),
"medium" => Ok(Self::Medium),
"low" => Ok(Self::Low),
"info" => Ok(Self::Info),
_ => Err(SeverityParseError {
value: s.to_string(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct Finding {
pub detector: String,
pub severity: Severity,
pub file: PathBuf,
#[serde(serialize_with = "serialize_line_number")]
pub line: LineNumber,
#[serde(serialize_with = "serialize_char_column")]
pub column: CharColumn,
pub message: String,
pub evidence: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct FindingLocation {
pub file: PathBuf,
pub line: LineNumber,
pub column: CharColumn,
}
impl FindingLocation {
#[must_use]
pub fn new(file: impl Into<PathBuf>, line: LineNumber, column: CharColumn) -> Self {
Self {
file: file.into(),
line,
column,
}
}
}
impl Finding {
#[must_use]
pub fn new(
detector: impl Into<String>,
severity: Severity,
location: FindingLocation,
message: impl Into<String>,
evidence: impl Into<String>,
) -> Self {
Self {
detector: detector.into(),
severity,
file: location.file,
line: location.line,
column: location.column,
message: message.into(),
evidence: evidence.into(),
}
}
}
pub trait Detector {
fn name(&self) -> &str;
fn severity(&self) -> Severity;
fn description(&self) -> &str;
fn detect(&self, module: &DamlModule) -> Vec<Finding> {
self.try_detect(module)
.unwrap_or_else(|e| panic!("detector '{}' failed: {}", self.name(), e))
}
fn try_detect(&self, module: &DamlModule) -> Result<Vec<Finding>, DetectError>;
}
pub struct ConfiguredDetector {
inner: Box<dyn Detector>,
name_override: Option<String>,
severity_override: Option<Severity>,
}
impl ConfiguredDetector {
fn new(
inner: Box<dyn Detector>,
name_override: Option<String>,
severity_override: Option<Severity>,
) -> Self {
Self {
inner,
name_override,
severity_override,
}
}
#[must_use]
pub fn with_name(inner: Box<dyn Detector>, name: impl Into<String>) -> Self {
Self::new(inner, Some(name.into()), None)
}
#[must_use]
pub fn with_severity(inner: Box<dyn Detector>, severity: Severity) -> Self {
Self::new(inner, None, Some(severity))
}
fn apply_overrides(&self, mut findings: Vec<Finding>) -> Vec<Finding> {
let name = self.name().to_string();
let severity = self.severity();
for finding in &mut findings {
if self.name_override.is_some() {
finding.detector = name.clone();
}
if self.severity_override.is_some() {
finding.severity = severity;
}
}
findings
}
}
impl Detector for ConfiguredDetector {
fn name(&self) -> &str {
self.name_override
.as_deref()
.unwrap_or_else(|| self.inner.name())
}
fn severity(&self) -> Severity {
self.severity_override
.unwrap_or_else(|| self.inner.severity())
}
fn description(&self) -> &str {
self.inner.description()
}
fn try_detect(&self, module: &DamlModule) -> Result<Vec<Finding>, DetectError> {
self.inner
.try_detect(module)
.map(|findings| self.apply_overrides(findings))
.map_err(|e| {
let wrapped_detector = e.detector().to_string();
let message = format!(
"wrapped detector '{wrapped_detector}' failed: {}",
e.message()
);
DetectError::with_source(self.name(), message, e)
})
}
}
#[must_use]
pub fn find_duplicate_detector_name(detectors: &[Box<dyn Detector>]) -> Option<String> {
let mut seen = std::collections::HashSet::new();
for det in detectors {
if !seen.insert(det.name()) {
return Some(det.name().to_string());
}
}
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod configured_detector_tests {
use super::*;
use std::path::PathBuf;
#[test]
fn configured_detector_with_name_rewrites_detector_name_and_findings() {
struct NamedDetector;
impl Detector for NamedDetector {
fn name(&self) -> &str {
"named-base"
}
fn severity(&self) -> Severity {
Severity::High
}
fn description(&self) -> &str {
"base description"
}
fn try_detect(
&self,
module: &crate::ir::DamlModule,
) -> Result<Vec<Finding>, DetectError> {
let _ = module;
Ok(vec![Finding::new(
self.name(),
self.severity(),
FindingLocation::new("named.daml", LineNumber::new(9), CharColumn::new(11)),
"rewritable finding",
"x",
)])
}
}
let detector: Box<dyn Detector> = Box::new(ConfiguredDetector::with_name(
Box::new(NamedDetector),
"rewrite",
));
assert_eq!(detector.name(), "rewrite");
let module = crate::ir::DamlModule {
ir_version: 8,
name: String::from("Main"),
file: PathBuf::from("named.daml"),
source: String::new(),
imports: Vec::new(),
templates: Vec::new(),
interfaces: Vec::new(),
functions: Vec::new(),
};
let findings = detector.try_detect(&module).unwrap();
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].detector, "rewrite");
assert_eq!(findings[0].line, LineNumber::new(9));
}
#[test]
fn configured_detector_with_severity_overrides_reported_severity() {
struct SeverityDetector;
impl Detector for SeverityDetector {
fn name(&self) -> &str {
"severity-base"
}
fn severity(&self) -> Severity {
Severity::Critical
}
fn description(&self) -> &str {
"base description"
}
fn try_detect(
&self,
module: &crate::ir::DamlModule,
) -> Result<Vec<Finding>, DetectError> {
let _ = module;
Ok(vec![Finding::new(
self.name(),
self.severity(),
FindingLocation::new("severity.daml", LineNumber::new(3), CharColumn::new(7)),
"high-severity finding",
"y",
)])
}
}
let detector: Box<dyn Detector> = Box::new(ConfiguredDetector::with_severity(
Box::new(SeverityDetector),
Severity::Info,
));
assert_eq!(detector.severity(), Severity::Info);
let module = crate::ir::DamlModule {
ir_version: 8,
name: String::from("Main"),
file: PathBuf::from("severity.daml"),
source: String::new(),
imports: Vec::new(),
templates: Vec::new(),
interfaces: Vec::new(),
functions: Vec::new(),
};
let findings = detector.try_detect(&module).unwrap();
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].severity, Severity::Info);
}
#[test]
fn configured_detector_preserves_wrapped_error_source_chain() {
struct FailingDetector;
impl Detector for FailingDetector {
fn name(&self) -> &str {
"failing-base"
}
fn severity(&self) -> Severity {
Severity::High
}
fn description(&self) -> &str {
"base description"
}
fn try_detect(
&self,
module: &crate::ir::DamlModule,
) -> Result<Vec<Finding>, DetectError> {
let _ = module;
Err(DetectError::with_source(
self.name(),
"could not run visitor",
std::io::Error::new(std::io::ErrorKind::Interrupted, "runtime stopped"),
))
}
}
let detector: Box<dyn Detector> = Box::new(ConfiguredDetector::with_name(
Box::new(FailingDetector),
"configured-name",
));
let module = crate::ir::DamlModule {
ir_version: 8,
name: String::from("Main"),
file: PathBuf::from("failing.daml"),
source: String::new(),
imports: Vec::new(),
templates: Vec::new(),
interfaces: Vec::new(),
functions: Vec::new(),
};
let err = detector.try_detect(&module).unwrap_err();
assert_eq!(err.detector(), "configured-name");
assert!(err.message().contains("failing-base"));
let wrapped = std::error::Error::source(&err)
.and_then(|source| source.downcast_ref::<DetectError>())
.expect("configured detector should preserve the inner DetectError");
assert_eq!(wrapped.detector(), "failing-base");
assert!(std::error::Error::source(wrapped)
.is_some_and(|source| source.to_string() == "runtime stopped"));
}
}