1use serde::Serialize;
14
15use crate::ast::Span;
16use crate::error::Diagnostic;
17
18#[derive(Debug, Clone, Default, PartialEq, Serialize)]
20pub struct Annotation {
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub label: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub message: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub id: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub description: Option<String>,
33 pub span: Span,
35}
36
37impl Annotation {
38 pub(crate) fn merge(&mut self, later: Annotation) {
40 if later.label.is_some() {
41 self.label = later.label;
42 }
43 if later.message.is_some() {
44 self.message = later.message;
45 }
46 if later.id.is_some() {
47 self.id = later.id;
48 }
49 if later.description.is_some() {
50 self.description = later.description;
51 }
52 self.span = self.span.to(later.span);
53 }
54}
55
56#[derive(Debug, Clone, PartialEq)]
58pub(crate) struct DocComment {
59 pub span: Span,
60 pub body_start: usize,
61 pub body: String,
62}
63
64pub(crate) fn parse_doc(doc: &DocComment, warnings: &mut Vec<Diagnostic>) -> Annotation {
66 let mut annotation = Annotation {
67 span: doc.span,
68 ..Annotation::default()
69 };
70 let text = doc.body.as_str();
71 let mut description = String::new();
72 let mut i = 0;
73 let bytes = text.as_bytes();
74
75 while i < bytes.len() {
77 if bytes[i] == b'@'
78 && (i == 0 || bytes[i - 1].is_ascii_whitespace() || bytes[i - 1] == b'*')
79 {
80 break;
81 }
82 i += 1;
83 }
84 for line in text[..i].lines() {
85 let line = line.trim().trim_start_matches('*').trim();
86 if !line.is_empty() {
87 if !description.is_empty() {
88 description.push(' ');
89 }
90 description.push_str(line);
91 }
92 }
93 if !description.is_empty() {
94 annotation.description = Some(description);
95 }
96
97 while i < bytes.len() {
99 if bytes[i] != b'@' {
100 i += 1;
101 continue;
102 }
103 let tag_start = i;
104 i += 1;
105 let name_start = i;
106 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
107 i += 1;
108 }
109 let tag = &text[name_start..i];
110 let tag_span = Span::new(doc.body_start + tag_start, doc.body_start + i);
111 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
112 i += 1;
113 }
114 let value = if i < bytes.len() && bytes[i] == b'"' {
115 match json_string_end(&bytes[i..]) {
116 Some(len) => {
117 let literal = &text[i..i + len];
118 i += len;
119 serde_json::from_str::<String>(literal).ok()
120 }
121 None => None,
122 }
123 } else {
124 None
125 };
126 let value = match value {
127 Some(v) => v,
128 None => {
129 let start = i;
131 while i < bytes.len() && bytes[i] != b'\n' && bytes[i] != b'@' {
132 i += 1;
133 }
134 let bare = text[start..i]
135 .trim()
136 .trim_end_matches('*')
137 .trim()
138 .to_owned();
139 if matches!(tag, "label" | "message" | "id") {
140 warnings.push(Diagnostic::warning("W0103", tag_span, &[("tag", tag)]));
141 }
142 bare
143 }
144 };
145 match tag {
146 "label" => annotation.label = Some(value),
147 "message" => annotation.message = Some(value),
148 "id" => annotation.id = Some(value),
149 _ => warnings.push(Diagnostic::warning("W0102", tag_span, &[("tag", tag)])),
150 }
151 }
152 annotation
153}
154
155fn json_string_end(bytes: &[u8]) -> Option<usize> {
157 let mut i = 1;
158 while i < bytes.len() {
159 match bytes[i] {
160 b'\\' => i += 2,
161 b'"' => return Some(i + 1),
162 b'\n' => return None,
163 _ => i += 1,
164 }
165 }
166 None
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 fn parse(body: &str) -> (Annotation, Vec<Diagnostic>) {
174 let doc = DocComment {
175 span: Span::new(0, body.len() + 5),
176 body_start: 3,
177 body: body.to_owned(),
178 };
179 let mut warnings = Vec::new();
180 let a = parse_doc(&doc, &mut warnings);
181 (a, warnings)
182 }
183
184 #[test]
185 fn reads_tags_and_description() {
186 let (a, w) = parse(
187 "\n * Checks the customer.\n * @label \"Authorized customer\"\n * @message \"Login \\\"must\\\" match\" @id \"R-1\"\n ",
188 );
189 assert!(w.is_empty(), "{w:?}");
190 assert_eq!(a.description.as_deref(), Some("Checks the customer."));
191 assert_eq!(a.label.as_deref(), Some("Authorized customer"));
192 assert_eq!(a.message.as_deref(), Some("Login \"must\" match"));
193 assert_eq!(a.id.as_deref(), Some("R-1"));
194 }
195
196 #[test]
197 fn warns_on_unknown_tags_and_bare_values() {
198 let (a, w) = parse(" @label Total above limit @severity \"high\" ");
199 assert_eq!(a.label.as_deref(), Some("Total above limit"));
200 let codes: Vec<_> = w.iter().map(|d| d.code.as_str()).collect();
201 assert_eq!(codes, ["W0103", "W0102"]);
202 }
203}