harn_parser/
stdlib_metadata.rs1use harn_lexer::Span;
23
24#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
33#[serde(rename_all = "snake_case")]
34pub struct StdlibMetadata {
35 pub effects: Option<Vec<String>>,
38 pub allocation: Option<String>,
42 pub errors: Option<Vec<String>>,
44 pub api_stability: Option<String>,
46 pub example: Option<String>,
48}
49
50impl StdlibMetadata {
51 pub fn is_complete(&self) -> bool {
53 self.effects.is_some()
54 && self.allocation.is_some()
55 && self.errors.is_some()
56 && self.api_stability.is_some()
57 && self.example.is_some()
58 }
59
60 pub fn is_empty(&self) -> bool {
63 self.effects.is_none()
64 && self.allocation.is_none()
65 && self.errors.is_none()
66 && self.api_stability.is_none()
67 && self.example.is_none()
68 }
69
70 pub fn missing_fields(&self) -> Vec<&'static str> {
72 let mut out: Vec<&'static str> = Vec::new();
73 if self.effects.is_none() {
74 out.push("effects");
75 }
76 if self.allocation.is_none() {
77 out.push("allocation");
78 }
79 if self.errors.is_none() {
80 out.push("errors");
81 }
82 if self.api_stability.is_none() {
83 out.push("api_stability");
84 }
85 if self.example.is_none() {
86 out.push("example");
87 }
88 out
89 }
90
91 pub fn to_markdown(&self) -> String {
95 if self.is_empty() {
96 return String::new();
97 }
98 let mut lines: Vec<String> = Vec::new();
99 if let Some(effects) = &self.effects {
100 lines.push(format!(
101 "- **effects:** {}",
102 if effects.is_empty() {
103 "_none_".to_string()
104 } else {
105 effects
106 .iter()
107 .map(|e| format!("`{e}`"))
108 .collect::<Vec<_>>()
109 .join(", ")
110 }
111 ));
112 }
113 if let Some(allocation) = &self.allocation {
114 lines.push(format!("- **allocation:** `{allocation}`"));
115 }
116 if let Some(errors) = &self.errors {
117 lines.push(format!(
118 "- **errors:** {}",
119 if errors.is_empty() {
120 "_none_".to_string()
121 } else {
122 errors
123 .iter()
124 .map(|e| format!("`{e}`"))
125 .collect::<Vec<_>>()
126 .join(", ")
127 }
128 ));
129 }
130 if let Some(stability) = &self.api_stability {
131 lines.push(format!("- **api_stability:** `{stability}`"));
132 }
133 if let Some(example) = &self.example {
134 lines.push(format!("- **example:**\n\n```harn\n{example}\n```"));
135 }
136 format!("**Stdlib metadata**\n\n{}", lines.join("\n"))
137 }
138}
139
140pub fn parse_from_doc_body(body: &str) -> StdlibMetadata {
146 parse_from_doc_lines(&body.lines().collect::<Vec<_>>())
147}
148
149fn parse_from_doc_lines(lines: &[&str]) -> StdlibMetadata {
150 let mut meta = StdlibMetadata::default();
151 let mut current_key: Option<&'static str> = None;
152 let mut current_value: String = String::new();
153
154 let flush = |key: Option<&'static str>, value: String, meta: &mut StdlibMetadata| {
155 let Some(key) = key else { return };
156 let trimmed = value.trim_end_matches('\n').to_string();
157 assign_field(meta, key, &trimmed);
158 };
159
160 for raw in lines {
161 let line = raw.trim();
162 if let Some((key, rest)) = parse_key_line(line) {
163 flush(current_key, std::mem::take(&mut current_value), &mut meta);
165 current_key = Some(key);
166 current_value.clear();
167 current_value.push_str(rest.trim());
168 } else if current_key.is_some() {
169 if line.is_empty() {
173 flush(current_key, std::mem::take(&mut current_value), &mut meta);
174 current_key = None;
175 } else if current_key == Some("example") {
176 current_value.push('\n');
177 current_value.push_str(line);
178 }
179 }
180 }
181 flush(current_key, current_value, &mut meta);
182 meta
183}
184
185fn parse_key_line(line: &str) -> Option<(&'static str, &str)> {
186 let rest = line.strip_prefix('@')?;
187 let colon = rest.find(':')?;
188 let (key, after) = rest.split_at(colon);
189 let key = match key.trim() {
190 "effects" => "effects",
191 "allocation" => "allocation",
192 "errors" => "errors",
193 "api_stability" => "api_stability",
194 "example" => "example",
195 _ => return None,
196 };
197 Some((key, &after[1..]))
198}
199
200fn assign_field(meta: &mut StdlibMetadata, key: &str, value: &str) {
201 match key {
202 "effects" => meta.effects = Some(parse_list(value)),
203 "errors" => meta.errors = Some(parse_list(value)),
204 "allocation" => meta.allocation = Some(value.trim().to_string()),
205 "api_stability" => meta.api_stability = Some(value.trim().to_string()),
206 "example" => meta.example = Some(value.trim().to_string()),
207 _ => {}
208 }
209}
210
211fn parse_list(raw: &str) -> Vec<String> {
212 let trimmed = raw.trim();
213 let stripped = trimmed
214 .strip_prefix('[')
215 .and_then(|s| s.strip_suffix(']'))
216 .unwrap_or(trimmed);
217 stripped
218 .split(',')
219 .map(|part| part.trim().to_string())
220 .filter(|part| !part.is_empty())
221 .collect()
222}
223
224pub fn parse_for_span(source: &str, span: &Span) -> Option<StdlibMetadata> {
229 let body = extract_doc_body(source, span)?;
230 Some(parse_from_doc_body(&body))
231}
232
233fn extract_doc_body(source: &str, span: &Span) -> Option<String> {
234 let lines: Vec<&str> = source.lines().collect();
235 let def_line_idx = span.line.checked_sub(1)?;
236 if def_line_idx == 0 {
237 return None;
238 }
239 let above_idx = def_line_idx - 1;
240 let above = lines.get(above_idx)?.trim_end();
241 if !above.trim_end().ends_with("*/") {
242 return None;
243 }
244
245 let above_trim = above.trim_start();
247 if above_trim.starts_with("/**") && above_trim.ends_with("*/") && above_trim.len() >= 5 {
248 let inner = &above_trim[3..above_trim.len() - 2];
249 return Some(inner.trim().to_string());
250 }
251
252 let mut start_idx = above_idx;
254 loop {
255 let line = lines.get(start_idx)?.trim_start();
256 if line.starts_with("/**") {
257 break;
258 }
259 if start_idx == 0 {
260 return None;
261 }
262 start_idx -= 1;
263 }
264 let mut body = String::new();
265 for (i, line) in lines.iter().enumerate().take(above_idx + 1).skip(start_idx) {
266 let trimmed = line.trim();
267 let stripped = if i == start_idx {
268 trimmed.strip_prefix("/**").unwrap_or(trimmed).trim_start()
269 } else if i == above_idx {
270 let without_tail = trimmed.strip_suffix("*/").unwrap_or(trimmed).trim_end();
271 without_tail
272 .strip_prefix('*')
273 .map(|s| s.strip_prefix(' ').unwrap_or(s))
274 .unwrap_or(without_tail)
275 } else {
276 trimmed
277 .strip_prefix('*')
278 .map(|s| s.strip_prefix(' ').unwrap_or(s))
279 .unwrap_or(trimmed)
280 };
281 if !body.is_empty() {
282 body.push('\n');
283 }
284 body.push_str(stripped);
285 }
286 Some(body)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn parses_all_five_fields_inline() {
295 let body = "Reads a file.\n\n@effects: [fs.read]\n@allocation: heap\n@errors: [FileNotFound, PermissionDenied]\n@api_stability: stable\n@example: let s = fs::read_to_string(harness.fs, \"/x\")";
296 let meta = parse_from_doc_body(body);
297 assert!(meta.is_complete(), "missing: {:?}", meta.missing_fields());
298 assert_eq!(meta.effects.as_deref(), Some(&["fs.read".to_string()][..]));
299 assert_eq!(meta.allocation.as_deref(), Some("heap"));
300 assert_eq!(
301 meta.errors.as_deref(),
302 Some(&["FileNotFound".to_string(), "PermissionDenied".to_string()][..]),
303 );
304 assert_eq!(meta.api_stability.as_deref(), Some("stable"));
305 assert_eq!(
306 meta.example.as_deref(),
307 Some("let s = fs::read_to_string(harness.fs, \"/x\")"),
308 );
309 }
310
311 #[test]
312 fn partial_metadata_lists_missing_fields() {
313 let body = "@effects: []\n@api_stability: experimental";
314 let meta = parse_from_doc_body(body);
315 assert!(!meta.is_complete());
316 assert!(!meta.is_empty());
317 assert_eq!(
318 meta.missing_fields(),
319 vec!["allocation", "errors", "example"],
320 );
321 }
322
323 #[test]
324 fn empty_effect_and_error_lists_are_explicit() {
325 let body = "@effects: []\n@errors: []";
326 let meta = parse_from_doc_body(body);
327 assert_eq!(meta.effects.as_deref(), Some(&[][..]));
328 assert_eq!(meta.errors.as_deref(), Some(&[][..]));
329 }
330
331 #[test]
332 fn unknown_keys_do_not_pollute_storage() {
333 let body = "@deprecated: yes\n@allocation: stack-only";
334 let meta = parse_from_doc_body(body);
335 assert_eq!(meta.allocation.as_deref(), Some("stack-only"));
336 assert!(meta.effects.is_none());
338 }
339
340 #[test]
341 fn example_continuation_lines_are_joined() {
342 let body = "@example: let s = fs::open(p)\n let b = fs::read(s)\n fs::close(s)";
343 let meta = parse_from_doc_body(body);
344 assert_eq!(
345 meta.example.as_deref(),
346 Some("let s = fs::open(p)\nlet b = fs::read(s)\nfs::close(s)"),
347 );
348 }
349
350 #[test]
351 fn parse_for_span_extracts_multi_line_block() {
352 let source = "\
353/**
354 * Read the file.
355 *
356 * @effects: [fs.read]
357 * @allocation: heap
358 * @errors: [FileNotFound]
359 * @api_stability: stable
360 * @example: fs::read(\"/x\")
361 */
362pub fn read_file(path) {
363 __fs_read_to_string(path)
364}
365";
366 let span = Span::with_offsets(0, 0, 10, 1);
367 let meta = parse_for_span(source, &span).expect("metadata present");
368 assert!(meta.is_complete(), "missing: {:?}", meta.missing_fields());
369 }
370
371 #[test]
372 fn parse_for_span_handles_single_line_block() {
373 let source = "/** @effects: [] @allocation: stack-only @errors: [] @api_stability: stable @example: noop() */\npub fn noop() { }\n";
374 let span = Span::with_offsets(0, 0, 2, 1);
375 let meta = parse_for_span(source, &span).expect("metadata present");
376 assert!(!meta.is_empty());
378 }
379
380 #[test]
381 fn markdown_omits_unset_fields() {
382 let meta = StdlibMetadata {
383 effects: Some(vec!["fs.read".to_string()]),
384 allocation: Some("heap".to_string()),
385 errors: None,
386 api_stability: Some("stable".to_string()),
387 example: None,
388 };
389 let md = meta.to_markdown();
390 assert!(md.contains("**effects:**"));
391 assert!(md.contains("**allocation:**"));
392 assert!(md.contains("**api_stability:**"));
393 assert!(!md.contains("**errors:**"));
394 assert!(!md.contains("**example:**"));
395 }
396}