use crate::consts;
use crate::span::{SourceSpan, UNKNOWN_SPAN};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefectPhase {
Parse,
Lint,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParseDefect {
pub rule_id: String,
pub message: String,
pub span: SourceSpan,
pub resource_id: Option<String>,
pub property_path: Option<String>,
pub phase: Option<DefectPhase>,
}
impl ParseDefect {
pub fn new(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
rule_id: rule_id.into(),
message: message.into(),
span: UNKNOWN_SPAN,
resource_id: None,
property_path: None,
phase: None,
}
}
#[must_use]
pub fn location(mut self, span: SourceSpan) -> Self {
self.span = span;
self
}
#[must_use]
pub fn resource(mut self, resource_id: impl Into<String>) -> Self {
let id = resource_id.into();
if !id.is_empty() {
self.resource_id = Some(id);
}
self
}
#[must_use]
pub fn property_path(mut self, property_path: impl Into<String>) -> Self {
let path = property_path.into();
if !path.is_empty() {
self.property_path = Some(path);
}
self
}
#[must_use]
pub fn phase(mut self, phase: DefectPhase) -> Self {
self.phase = Some(phase);
self
}
#[must_use]
pub fn is_fatal(&self) -> bool {
self.rule_id.starts_with('F')
}
#[must_use]
pub fn resource_logical_id(&self) -> Option<&str> {
self.resource_id.as_deref()
}
}
pub(crate) fn make_parse_defect_for_resource(
rule_id: &str,
message: String,
span: SourceSpan,
resource_id: &str,
) -> ParseDefect {
ParseDefect::new(rule_id, message).location(span).resource(resource_id).phase(DefectPhase::Parse)
}
pub(crate) fn make_parse_defect(rule_id: &str, message: String, span: SourceSpan) -> ParseDefect {
ParseDefect::new(rule_id, message).location(span).phase(DefectPhase::Parse)
}
pub(crate) fn make_parse_defect_at(rule_id: &str, message: String, span: SourceSpan, build_path: &str) -> ParseDefect {
let mut defect = ParseDefect::new(rule_id, message).location(span).phase(DefectPhase::Parse);
let segments: Vec<&str> = build_path.split('/').collect();
let is_resource_value = segments.len() >= 4
&& segments[0] == consts::SECTION_RESOURCES
&& matches!(segments[2], consts::KEY_PROPERTIES | consts::SECTION_METADATA);
let is_lifecycle_attribute = segments.len() >= 3
&& segments[0] == consts::SECTION_RESOURCES
&& matches!(
segments[2],
consts::KEY_CREATION_POLICY
| consts::KEY_UPDATE_POLICY
| consts::KEY_DELETION_POLICY
| consts::KEY_UPDATE_REPLACE_POLICY
);
if is_resource_value || is_lifecycle_attribute {
defect = defect.resource(segments[1]);
defect = defect.property_path(segments[2..].join("."));
} else if segments.len() >= 2 {
defect = defect.property_path(build_path);
}
defect
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_resource_id_and_property_path_are_dropped() {
let defect = ParseDefect::new("F0001", "msg").resource("").property_path("");
assert_eq!(defect.resource_id, None, "an empty resource ID must not create an anchor");
assert_eq!(defect.property_path, None, "an empty property path must not be recorded");
}
#[test]
fn is_fatal_follows_the_rule_id_prefix() {
assert!(ParseDefect::new("F8600", "msg").is_fatal());
assert!(!ParseDefect::new("E0001", "msg").is_fatal());
assert!(!ParseDefect::new("W1103", "msg").is_fatal());
}
#[test]
fn make_parse_defect_at_splits_resource_property_paths() {
let defect = make_parse_defect_at("F1032", "msg".into(), UNKNOWN_SPAN, "Resources/R/Properties/X/Fn::If");
assert_eq!(defect.resource_id.as_deref(), Some("R"));
assert_eq!(defect.property_path.as_deref(), Some("Properties.X.Fn::If"));
assert_eq!(defect.phase, Some(DefectPhase::Parse));
}
#[test]
fn make_parse_defect_at_splits_lifecycle_attribute_paths() {
let defect = make_parse_defect_at("F1101", "msg".into(), UNKNOWN_SPAN, "Resources/R/DeletionPolicy/Fn::If/1");
assert_eq!(defect.resource_id.as_deref(), Some("R"));
assert_eq!(defect.property_path.as_deref(), Some("DeletionPolicy.Fn::If.1"));
assert_eq!(defect.phase, Some(DefectPhase::Parse));
}
#[test]
fn make_parse_defect_at_keeps_non_resource_paths_verbatim() {
let defect = make_parse_defect_at("E8005", "msg".into(), UNKNOWN_SPAN, "Conditions/C/Fn::And");
assert_eq!(defect.resource_id, None);
assert_eq!(defect.property_path.as_deref(), Some("Conditions/C/Fn::And"));
}
}