1use crate::consts;
10use crate::span::{SourceSpan, UNKNOWN_SPAN};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum DefectPhase {
21 Parse,
22 Lint,
23}
24
25#[derive(Debug, Clone, PartialEq)]
31pub struct ParseDefect {
32 pub rule_id: String,
34 pub message: String,
35 pub span: SourceSpan,
37 pub resource_id: Option<String>,
40 pub property_path: Option<String>,
42 pub phase: Option<DefectPhase>,
44}
45
46impl ParseDefect {
47 pub fn new(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
50 Self {
51 rule_id: rule_id.into(),
52 message: message.into(),
53 span: UNKNOWN_SPAN,
54 resource_id: None,
55 property_path: None,
56 phase: None,
57 }
58 }
59
60 #[must_use]
62 pub fn location(mut self, span: SourceSpan) -> Self {
63 self.span = span;
64 self
65 }
66
67 #[must_use]
70 pub fn resource(mut self, resource_id: impl Into<String>) -> Self {
71 let id = resource_id.into();
72 if !id.is_empty() {
73 self.resource_id = Some(id);
74 }
75 self
76 }
77
78 #[must_use]
81 pub fn property_path(mut self, property_path: impl Into<String>) -> Self {
82 let path = property_path.into();
83 if !path.is_empty() {
84 self.property_path = Some(path);
85 }
86 self
87 }
88
89 #[must_use]
91 pub fn phase(mut self, phase: DefectPhase) -> Self {
92 self.phase = Some(phase);
93 self
94 }
95
96 #[must_use]
100 pub fn is_fatal(&self) -> bool {
101 self.rule_id.starts_with('F')
102 }
103
104 #[must_use]
106 pub fn resource_logical_id(&self) -> Option<&str> {
107 self.resource_id.as_deref()
108 }
109}
110
111pub(crate) fn make_parse_defect_for_resource(
115 rule_id: &str,
116 message: String,
117 span: SourceSpan,
118 resource_id: &str,
119) -> ParseDefect {
120 ParseDefect::new(rule_id, message).location(span).resource(resource_id).phase(DefectPhase::Parse)
121}
122
123pub(crate) fn make_parse_defect(rule_id: &str, message: String, span: SourceSpan) -> ParseDefect {
124 ParseDefect::new(rule_id, message).location(span).phase(DefectPhase::Parse)
125}
126
127pub(crate) fn make_parse_defect_at(rule_id: &str, message: String, span: SourceSpan, build_path: &str) -> ParseDefect {
136 let mut defect = ParseDefect::new(rule_id, message).location(span).phase(DefectPhase::Parse);
137 let segments: Vec<&str> = build_path.split('/').collect();
138 let is_resource_value = segments.len() >= 4
139 && segments[0] == consts::SECTION_RESOURCES
140 && matches!(segments[2], consts::KEY_PROPERTIES | consts::SECTION_METADATA);
141 let is_lifecycle_attribute = segments.len() >= 3
142 && segments[0] == consts::SECTION_RESOURCES
143 && matches!(
144 segments[2],
145 consts::KEY_CREATION_POLICY
146 | consts::KEY_UPDATE_POLICY
147 | consts::KEY_DELETION_POLICY
148 | consts::KEY_UPDATE_REPLACE_POLICY
149 );
150 if is_resource_value || is_lifecycle_attribute {
151 defect = defect.resource(segments[1]);
152 defect = defect.property_path(segments[2..].join("."));
153 } else if segments.len() >= 2 {
154 defect = defect.property_path(build_path);
157 }
158 defect
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn empty_resource_id_and_property_path_are_dropped() {
167 let defect = ParseDefect::new("F0001", "msg").resource("").property_path("");
168 assert_eq!(defect.resource_id, None, "an empty resource ID must not create an anchor");
169 assert_eq!(defect.property_path, None, "an empty property path must not be recorded");
170 }
171
172 #[test]
173 fn is_fatal_follows_the_rule_id_prefix() {
174 assert!(ParseDefect::new("F8600", "msg").is_fatal());
175 assert!(!ParseDefect::new("E0001", "msg").is_fatal());
176 assert!(!ParseDefect::new("W1103", "msg").is_fatal());
177 }
178
179 #[test]
180 fn make_parse_defect_at_splits_resource_property_paths() {
181 let defect = make_parse_defect_at("F1032", "msg".into(), UNKNOWN_SPAN, "Resources/R/Properties/X/Fn::If");
182 assert_eq!(defect.resource_id.as_deref(), Some("R"));
183 assert_eq!(defect.property_path.as_deref(), Some("Properties.X.Fn::If"));
184 assert_eq!(defect.phase, Some(DefectPhase::Parse));
185 }
186
187 #[test]
188 fn make_parse_defect_at_splits_lifecycle_attribute_paths() {
189 let defect = make_parse_defect_at("F1101", "msg".into(), UNKNOWN_SPAN, "Resources/R/DeletionPolicy/Fn::If/1");
190 assert_eq!(defect.resource_id.as_deref(), Some("R"));
191 assert_eq!(defect.property_path.as_deref(), Some("DeletionPolicy.Fn::If.1"));
192 assert_eq!(defect.phase, Some(DefectPhase::Parse));
193 }
194
195 #[test]
196 fn make_parse_defect_at_keeps_non_resource_paths_verbatim() {
197 let defect = make_parse_defect_at("E8005", "msg".into(), UNKNOWN_SPAN, "Conditions/C/Fn::And");
198 assert_eq!(defect.resource_id, None);
199 assert_eq!(defect.property_path.as_deref(), Some("Conditions/C/Fn::And"));
200 }
201}