1use std::path::Path;
12use std::sync::LazyLock;
13
14use regex::Regex;
15use serde::{Deserialize, Serialize};
16
17use crate::cache::{
18 BehavioralAnnotations, DocumentationAnnotations, FileEntry, InlineAnnotation,
19 LifecycleAnnotations, MemoizedValue, PerformanceAnnotations, SymbolEntry, SymbolType, TypeInfo,
20 TypeParamInfo, TypeReturnInfo, TypeSource, TypeTypeParam, Visibility,
21};
22use crate::error::{AcpError, Result};
23use crate::index::detect_language;
24
25static ANNOTATION_PATTERN: LazyLock<Regex> =
29 LazyLock::new(|| Regex::new(r"@acp:([\w-]+)(?:\s+([^-\n]+?))?(?:\s+-\s+(.+))?$").unwrap());
30
31static CONTINUATION_PATTERN: LazyLock<Regex> =
33 LazyLock::new(|| Regex::new(r"^(?://|#|/?\*)\s{2,}(.+)$").unwrap());
34
35static SOURCE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
41 Regex::new(r"@acp:source\s+(explicit|converted|heuristic|refined|inferred)(?:\s+-\s+(.+))?$")
42 .unwrap()
43});
44
45static CONFIDENCE_PATTERN: LazyLock<Regex> =
47 LazyLock::new(|| Regex::new(r"@acp:source-confidence\s+(\d+\.?\d*)(?:\s+-\s+(.+))?$").unwrap());
48
49static REVIEWED_PATTERN: LazyLock<Regex> =
51 LazyLock::new(|| Regex::new(r"@acp:source-reviewed\s+(true|false)(?:\s+-\s+(.+))?$").unwrap());
52
53static ID_PATTERN: LazyLock<Regex> =
55 LazyLock::new(|| Regex::new(r"@acp:source-id\s+([a-zA-Z0-9\-]+)(?:\s+-\s+(.+))?$").unwrap());
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
59#[serde(rename_all = "lowercase")]
60pub enum SourceOrigin {
61 #[default]
63 Explicit,
64 Converted,
66 Heuristic,
68 Refined,
70 Inferred,
72}
73
74impl SourceOrigin {
75 pub fn as_str(&self) -> &'static str {
77 match self {
78 SourceOrigin::Explicit => "explicit",
79 SourceOrigin::Converted => "converted",
80 SourceOrigin::Heuristic => "heuristic",
81 SourceOrigin::Refined => "refined",
82 SourceOrigin::Inferred => "inferred",
83 }
84 }
85}
86
87impl std::str::FromStr for SourceOrigin {
88 type Err = String;
89
90 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
91 match s.to_lowercase().as_str() {
92 "explicit" => Ok(SourceOrigin::Explicit),
93 "converted" => Ok(SourceOrigin::Converted),
94 "heuristic" => Ok(SourceOrigin::Heuristic),
95 "refined" => Ok(SourceOrigin::Refined),
96 "inferred" => Ok(SourceOrigin::Inferred),
97 _ => Err(format!("Unknown source origin: {}", s)),
98 }
99 }
100}
101
102impl std::fmt::Display for SourceOrigin {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 write!(f, "{}", self.as_str())
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110pub struct ProvenanceMarker {
111 pub source: SourceOrigin,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub confidence: Option<f64>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub reviewed: Option<bool>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub generation_id: Option<String>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct AnnotationWithProvenance {
127 pub annotation: Annotation,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub provenance: Option<ProvenanceMarker>,
132}
133
134#[derive(Debug, Clone)]
136pub struct ParseResult {
137 pub file: FileEntry,
138 pub symbols: Vec<SymbolEntry>,
139 pub calls: Vec<(String, Vec<String>)>, pub lock_level: Option<String>, pub lock_directive: Option<String>, pub ai_hints: Vec<String>, pub hacks: Vec<HackAnnotation>, pub inline_annotations: Vec<InlineAnnotation>, pub purpose: Option<String>, pub owner: Option<String>, }
148
149#[derive(Debug, Clone)]
151pub struct HackAnnotation {
152 pub line: usize,
153 pub expires: Option<String>,
154 pub ticket: Option<String>,
155 pub reason: Option<String>,
156}
157
158pub struct Parser {
160 }
163
164impl Parser {
165 pub fn new() -> Self {
166 Self {}
167 }
168
169 pub fn parse<P: AsRef<Path>>(&self, path: P) -> Result<ParseResult> {
171 let path = path.as_ref();
172 let content = std::fs::read_to_string(path)?;
173 let file_path = path.to_string_lossy().to_string();
174
175 let language = detect_language(&file_path).ok_or_else(|| {
176 AcpError::UnsupportedLanguage(
177 path.extension()
178 .map(|e| e.to_string_lossy().to_string())
179 .unwrap_or_default(),
180 )
181 })?;
182
183 let lines = content.lines().count();
184 let _file_name = path
185 .file_stem()
186 .map(|s| s.to_string_lossy().to_string())
187 .unwrap_or_default();
188
189 let annotations = self.parse_annotations(&content);
191
192 let mut module_name = None;
194 let mut file_summary = None;
195 let mut domains = vec![];
196 let mut layer = None;
197 let mut symbols = vec![];
198 let mut exports = vec![];
199 let mut imports = vec![];
200 let mut calls = vec![];
201 let mut lock_level = None;
202 let mut lock_directive = None;
203 let mut ai_hints = vec![];
204 let mut hacks = vec![];
205 let mut inline_annotations = vec![];
206 let mut purpose = None;
207 let mut owner = None;
208
209 let mut file_version: Option<String> = None;
211 let mut file_since: Option<String> = None;
212 let mut file_license: Option<String> = None;
213 let mut file_author: Option<String> = None;
214 let mut file_lifecycle = LifecycleAnnotations::default();
215
216 let mut current_symbol: Option<SymbolBuilder> = None;
218
219 for ann in &annotations {
220 match ann.name.as_str() {
221 "module" => {
222 if let Some(val) = &ann.value {
223 module_name = Some(val.trim_matches('"').to_string());
224 }
225 }
226 "summary" => {
227 if let Some(ref mut builder) = current_symbol {
228 if let Some(val) = &ann.value {
229 builder.summary = Some(val.trim_matches('"').to_string());
230 }
231 } else if let Some(val) = &ann.value {
232 file_summary = Some(val.trim_matches('"').to_string());
234 }
235 }
236 "domain" => {
237 if let Some(val) = &ann.value {
238 domains.push(val.trim_matches('"').to_string());
239 }
240 }
241 "layer" => {
242 if let Some(val) = &ann.value {
243 layer = Some(val.trim_matches('"').to_string());
244 }
245 }
246 "lock" => {
247 if let Some(val) = &ann.value {
248 lock_level = Some(val.trim_matches('"').to_string());
249 }
250 lock_directive = ann.directive.clone();
252 }
253 "purpose" => {
255 if let Some(val) = &ann.value {
256 purpose = Some(val.trim_matches('"').to_string());
257 } else if let Some(dir) = &ann.directive {
258 purpose = Some(dir.clone());
259 }
260 }
261 "owner" => {
263 if let Some(val) = &ann.value {
264 owner = Some(val.trim_matches('"').to_string());
265 }
266 }
267 "ai-careful" | "ai-readonly" | "ai-avoid" | "ai-no-modify" => {
268 let hint = if let Some(val) = &ann.value {
269 format!("{}: {}", ann.name, val.trim_matches('"'))
270 } else {
271 ann.name.clone()
272 };
273 ai_hints.push(hint);
274 }
275 "hack" => {
276 let mut expires = None;
278 let mut ticket = None;
279 let mut reason = None;
280
281 if let Some(val) = &ann.value {
282 for part in val.split_whitespace() {
284 if let Some(exp) = part.strip_prefix("expires=") {
285 expires = Some(exp.to_string());
286 } else if let Some(tkt) = part.strip_prefix("ticket=") {
287 ticket = Some(tkt.to_string());
288 } else if part.starts_with('"') {
289 reason = Some(val.split('"').nth(1).unwrap_or("").to_string());
291 break;
292 }
293 }
294 }
295
296 let hack = HackAnnotation {
297 line: ann.line,
298 expires: expires.clone(),
299 ticket: ticket.clone(),
300 reason,
301 };
302 hacks.push(hack);
303
304 inline_annotations.push(InlineAnnotation {
306 line: ann.line,
307 annotation_type: "hack".to_string(),
308 value: ann.value.clone(),
309 directive: ann
310 .directive
311 .clone()
312 .unwrap_or_else(|| "Temporary workaround".to_string()),
313 expires,
314 ticket,
315 auto_generated: ann.auto_generated,
316 });
317 }
318 "todo" | "fixme" | "critical" => {
320 inline_annotations.push(InlineAnnotation {
321 line: ann.line,
322 annotation_type: ann.name.clone(),
323 value: ann.value.clone(),
324 directive: ann.directive.clone().unwrap_or_else(|| {
325 match ann.name.as_str() {
326 "todo" => "Pending work item".to_string(),
327 "fixme" => "Known issue requiring fix".to_string(),
328 "critical" => {
329 "Critical section - extra review required".to_string()
330 }
331 _ => "".to_string(),
332 }
333 }),
334 expires: None,
335 ticket: None,
336 auto_generated: ann.auto_generated,
337 });
338 if ann.name == "todo" {
340 if let Some(ref mut builder) = current_symbol {
341 let todo_text = ann
342 .directive
343 .clone()
344 .or_else(|| {
345 ann.value.clone().map(|v| v.trim_matches('"').to_string())
346 })
347 .unwrap_or_else(|| "Pending work item".to_string());
348 builder.documentation.todos.push(todo_text);
349 }
350 }
351 }
352 "perf" => {
354 inline_annotations.push(InlineAnnotation {
355 line: ann.line,
356 annotation_type: ann.name.clone(),
357 value: ann.value.clone(),
358 directive: ann
359 .directive
360 .clone()
361 .unwrap_or_else(|| "Performance-sensitive code".to_string()),
362 expires: None,
363 ticket: None,
364 auto_generated: ann.auto_generated,
365 });
366 if let Some(ref mut builder) = current_symbol {
368 if let Some(val) = &ann.value {
369 builder.performance.complexity =
370 Some(val.trim_matches('"').to_string());
371 }
372 }
373 }
374 "symbol" => {
375 if let Some(builder) = current_symbol.take() {
377 let sym = builder.build(&file_path);
378 exports.push(sym.name.clone());
379 symbols.push(sym);
380 }
381 if let Some(val) = &ann.value {
383 current_symbol = Some(SymbolBuilder::new(
384 val.trim_matches('"').to_string(),
385 ann.line,
386 &file_path,
387 ));
388 }
389 }
390 "fn" | "function" | "class" | "method" => {
392 if let Some(builder) = current_symbol.take() {
394 let sym = builder.build(&file_path);
395 exports.push(sym.name.clone());
396 symbols.push(sym);
397 }
398 if let Some(val) = &ann.value {
400 let mut builder = SymbolBuilder::new(
401 val.trim_matches('"').to_string(),
402 ann.line,
403 &file_path,
404 );
405 builder.symbol_type = match ann.name.as_str() {
406 "fn" | "function" => SymbolType::Function,
407 "class" => SymbolType::Class,
408 "method" => SymbolType::Method,
409 _ => SymbolType::Function,
410 };
411 builder.purpose = ann.directive.clone();
412 current_symbol = Some(builder);
413 }
414 }
415 "calls" => {
416 if let Some(ref mut builder) = current_symbol {
417 if let Some(val) = &ann.value {
418 let callees: Vec<String> = val
419 .split(',')
420 .map(|s| s.trim().trim_matches('"').to_string())
421 .collect();
422 builder.calls.extend(callees);
423 }
424 }
425 }
426 "imports" | "depends" => {
427 if let Some(val) = &ann.value {
428 let import_list: Vec<String> = val
429 .split(',')
430 .map(|s| s.trim().trim_matches('"').to_string())
431 .collect();
432 imports.extend(import_list);
433 }
434 }
435
436 "param" => {
440 if let Some(ref mut builder) = current_symbol {
441 if let Some(val) = &ann.value {
444 let val = val.trim();
445 let (type_expr, rest) = if val.starts_with('{') {
446 if let Some(close_idx) = val.find('}') {
448 let type_str = val[1..close_idx].trim().to_string();
449 let remaining = val[close_idx + 1..].trim();
450 (Some(type_str), remaining)
451 } else {
452 (None, val)
453 }
454 } else {
455 (None, val)
456 };
457
458 let (optional, name, default) = if rest.starts_with('[') {
460 if let Some(close_idx) = rest.find(']') {
462 let inner = &rest[1..close_idx];
463 if let Some(eq_idx) = inner.find('=') {
464 let n = inner[..eq_idx].trim().to_string();
465 let d = inner[eq_idx + 1..].trim().to_string();
466 (true, n, Some(d))
467 } else {
468 (true, inner.trim().to_string(), None)
469 }
470 } else {
471 (false, rest.trim_matches('"').to_string(), None)
472 }
473 } else {
474 let name = rest
476 .split_whitespace()
477 .next()
478 .unwrap_or("")
479 .trim_matches('"')
480 .to_string();
481 (false, name, None)
482 };
483
484 if !name.is_empty() {
485 builder.type_info.params.push(TypeParamInfo {
486 name,
487 r#type: type_expr.clone(),
488 type_source: type_expr.as_ref().map(|_| TypeSource::Acp),
489 optional,
490 default,
491 directive: ann.directive.clone(),
492 });
493 }
494 }
495 }
496 }
497 "returns" | "return" => {
498 if let Some(ref mut builder) = current_symbol {
499 let type_expr = ann.value.as_ref().and_then(|val| {
501 let val = val.trim();
502 if val.starts_with('{') {
503 val.find('}')
504 .map(|close_idx| val[1..close_idx].trim().to_string())
505 } else {
506 None
507 }
508 });
509
510 builder.type_info.returns = Some(TypeReturnInfo {
511 r#type: type_expr.clone(),
512 type_source: type_expr.as_ref().map(|_| TypeSource::Acp),
513 directive: ann.directive.clone(),
514 });
515 }
516 }
517 "template" => {
518 if let Some(ref mut builder) = current_symbol {
519 if let Some(val) = &ann.value {
521 let val = val.trim();
522 let (name, constraint) =
524 if let Some(extends_idx) = val.find(" extends ") {
525 let n = val[..extends_idx].trim().to_string();
526 let c = val[extends_idx + 9..].trim().to_string();
527 (n, Some(c))
528 } else {
529 (
530 val.split_whitespace().next().unwrap_or("").to_string(),
531 None,
532 )
533 };
534
535 if !name.is_empty() {
536 builder.type_info.type_params.push(TypeTypeParam {
537 name,
538 constraint,
539 directive: ann.directive.clone(),
540 });
541 }
542 }
543 }
544 }
545
546 "pure" => {
550 if let Some(ref mut builder) = current_symbol {
551 builder.behavioral.pure = true;
552 }
553 }
554 "idempotent" => {
555 if let Some(ref mut builder) = current_symbol {
556 builder.behavioral.idempotent = true;
557 }
558 }
559 "memoized" => {
560 if let Some(ref mut builder) = current_symbol {
561 if let Some(val) = &ann.value {
562 builder.behavioral.memoized =
563 Some(MemoizedValue::Duration(val.trim_matches('"').to_string()));
564 } else {
565 builder.behavioral.memoized = Some(MemoizedValue::Enabled(true));
566 }
567 }
568 }
569 "async" => {
570 if let Some(ref mut builder) = current_symbol {
571 builder.behavioral.r#async = true;
572 }
573 }
574 "generator" => {
575 if let Some(ref mut builder) = current_symbol {
576 builder.behavioral.generator = true;
577 }
578 }
579 "throttled" => {
580 if let Some(ref mut builder) = current_symbol {
581 if let Some(val) = &ann.value {
582 builder.behavioral.throttled = Some(val.trim_matches('"').to_string());
583 }
584 }
585 }
586 "transactional" => {
587 if let Some(ref mut builder) = current_symbol {
588 builder.behavioral.transactional = true;
589 }
590 }
591 "side-effects" => {
592 if let Some(ref mut builder) = current_symbol {
593 if let Some(val) = &ann.value {
594 let effects: Vec<String> = val
595 .split(',')
596 .map(|s| s.trim().trim_matches('"').to_string())
597 .collect();
598 builder.behavioral.side_effects.extend(effects);
599 }
600 }
601 }
602
603 "deprecated" => {
607 let message = ann
608 .directive
609 .clone()
610 .or_else(|| ann.value.clone().map(|v| v.trim_matches('"').to_string()))
611 .unwrap_or_else(|| "Deprecated".to_string());
612 if let Some(ref mut builder) = current_symbol {
613 builder.lifecycle.deprecated = Some(message);
614 } else {
615 file_lifecycle.deprecated = Some(message);
616 }
617 }
618 "experimental" => {
619 if let Some(ref mut builder) = current_symbol {
620 builder.lifecycle.experimental = true;
621 } else {
622 file_lifecycle.experimental = true;
623 }
624 }
625 "beta" => {
626 if let Some(ref mut builder) = current_symbol {
627 builder.lifecycle.beta = true;
628 } else {
629 file_lifecycle.beta = true;
630 }
631 }
632 "internal" => {
633 if let Some(ref mut builder) = current_symbol {
634 builder.lifecycle.internal = true;
635 } else {
636 file_lifecycle.internal = true;
637 }
638 }
639 "public-api" => {
640 if let Some(ref mut builder) = current_symbol {
641 builder.lifecycle.public_api = true;
642 } else {
643 file_lifecycle.public_api = true;
644 }
645 }
646 "since" => {
647 if let Some(val) = &ann.value {
648 let version = val.trim_matches('"').to_string();
649 if let Some(ref mut builder) = current_symbol {
650 builder.lifecycle.since = Some(version);
651 } else {
652 file_since = Some(version);
653 }
654 }
655 }
656
657 "example" => {
661 if let Some(ref mut builder) = current_symbol {
662 let example_text = ann
663 .directive
664 .clone()
665 .or_else(|| ann.value.clone().map(|v| v.trim_matches('"').to_string()))
666 .unwrap_or_default();
667 if !example_text.is_empty() {
668 builder.documentation.examples.push(example_text);
669 }
670 }
671 }
672 "see" => {
673 if let Some(ref mut builder) = current_symbol {
674 if let Some(val) = &ann.value {
675 builder
676 .documentation
677 .see_also
678 .push(val.trim_matches('"').to_string());
679 }
680 }
681 }
682 "link" => {
683 if let Some(ref mut builder) = current_symbol {
684 if let Some(val) = &ann.value {
685 builder
686 .documentation
687 .links
688 .push(val.trim_matches('"').to_string());
689 }
690 }
691 }
692 "note" => {
693 if let Some(ref mut builder) = current_symbol {
694 let note_text = ann
695 .directive
696 .clone()
697 .or_else(|| ann.value.clone().map(|v| v.trim_matches('"').to_string()))
698 .unwrap_or_default();
699 if !note_text.is_empty() {
700 builder.documentation.notes.push(note_text);
701 }
702 }
703 }
704 "warning" => {
705 if let Some(ref mut builder) = current_symbol {
706 let warning_text = ann
707 .directive
708 .clone()
709 .or_else(|| ann.value.clone().map(|v| v.trim_matches('"').to_string()))
710 .unwrap_or_default();
711 if !warning_text.is_empty() {
712 builder.documentation.warnings.push(warning_text);
713 }
714 }
715 }
716
717 "memory" => {
721 if let Some(ref mut builder) = current_symbol {
722 if let Some(val) = &ann.value {
723 builder.performance.memory = Some(val.trim_matches('"').to_string());
724 }
725 }
726 }
727 "cached" => {
728 if let Some(ref mut builder) = current_symbol {
729 if let Some(val) = &ann.value {
730 builder.performance.cached = Some(val.trim_matches('"').to_string());
731 } else {
732 builder.performance.cached = Some("true".to_string());
733 }
734 }
735 }
736
737 "version" => {
741 if let Some(val) = &ann.value {
742 file_version = Some(val.trim_matches('"').to_string());
743 }
744 }
745 "license" => {
746 if let Some(val) = &ann.value {
747 file_license = Some(val.trim_matches('"').to_string());
748 }
749 }
750 "author" => {
751 if let Some(val) = &ann.value {
752 file_author = Some(val.trim_matches('"').to_string());
753 }
754 }
755
756 _ => {}
757 }
758 }
759
760 if let Some(builder) = current_symbol {
762 let sym = builder.build(&file_path);
763 if !sym.calls.is_empty() {
764 calls.push((sym.name.clone(), sym.calls.clone()));
765 }
766 exports.push(sym.name.clone());
767 symbols.push(sym);
768 }
769
770 for sym in &symbols {
772 if !sym.calls.is_empty() {
773 calls.push((sym.name.clone(), sym.calls.clone()));
774 }
775 }
776
777 let file = FileEntry {
778 path: file_path,
779 lines,
780 language,
781 exports,
782 imports,
783 module: module_name,
784 summary: file_summary,
785 purpose: purpose.clone(),
786 owner: owner.clone(),
787 inline: inline_annotations.clone(),
788 domains,
789 layer,
790 stability: None,
791 ai_hints: ai_hints.clone(),
792 git: None,
793 annotations: std::collections::HashMap::new(), bridge: crate::cache::BridgeMetadata::default(), version: file_version,
797 since: file_since,
798 license: file_license,
799 author: file_author,
800 lifecycle: if file_lifecycle.is_empty() {
801 None
802 } else {
803 Some(file_lifecycle)
804 },
805 refs: Vec::new(),
807 style: None,
808 };
809
810 Ok(ParseResult {
811 file,
812 symbols,
813 calls,
814 lock_level,
815 lock_directive,
816 ai_hints,
817 hacks,
818 inline_annotations,
819 purpose,
820 owner,
821 })
822 }
823
824 pub fn parse_annotations(&self, content: &str) -> Vec<Annotation> {
827 let mut annotations = Vec::new();
828 let lines: Vec<&str> = content.lines().collect();
829 let mut i = 0;
830
831 while i < lines.len() {
832 let line = lines[i];
833 let line_1indexed = i + 1;
834
835 for cap in ANNOTATION_PATTERN.captures_iter(line) {
836 let name = cap.get(1).unwrap().as_str().to_string();
837 let value = cap.get(2).map(|m| m.as_str().trim().to_string());
838 let mut directive = cap.get(3).map(|m| m.as_str().trim().to_string());
839
840 let mut j = i + 1;
842 while j < lines.len() {
843 if let Some(cont_cap) = CONTINUATION_PATTERN.captures(lines[j]) {
844 let continuation = cont_cap.get(1).unwrap().as_str().trim();
845 if let Some(ref mut dir) = directive {
846 dir.push(' ');
847 dir.push_str(continuation);
848 } else {
849 directive = Some(continuation.to_string());
850 }
851 j += 1;
852 } else {
853 break;
854 }
855 }
856
857 let (final_directive, auto_generated) = match directive {
859 Some(d) if !d.is_empty() => (Some(d), false),
860 _ => (self.default_directive(&name, value.as_deref()), true),
861 };
862
863 annotations.push(Annotation {
864 name,
865 value,
866 directive: final_directive,
867 auto_generated,
868 line: line_1indexed,
869 });
870 }
871
872 i += 1;
873 }
874
875 annotations
876 }
877
878 fn default_directive(&self, name: &str, value: Option<&str>) -> Option<String> {
881 match name {
882 "lock" => match value {
883 Some("frozen") => Some("MUST NOT modify this code under any circumstances".into()),
884 Some("restricted") => {
885 Some("Explain proposed changes and wait for explicit approval".into())
886 }
887 Some("approval-required") => {
888 Some("Propose changes and request confirmation before applying".into())
889 }
890 Some("tests-required") => {
891 Some("All changes must include corresponding tests".into())
892 }
893 Some("docs-required") => Some("All changes must update documentation".into()),
894 Some("review-required") => {
895 Some("Changes require code review before merging".into())
896 }
897 Some("normal") | None => {
898 Some("Safe to modify following project conventions".into())
899 }
900 Some("experimental") => {
901 Some("Experimental code - changes welcome but may be unstable".into())
902 }
903 _ => None,
904 },
905 "ref" => value.map(|url| format!("Consult {} before making changes", url)),
906 "hack" => Some("Temporary workaround - check expiry before modifying".into()),
907 "deprecated" => Some("Do not use or extend - see replacement annotation".into()),
908 "todo" => Some("Pending work item - address before release".into()),
909 "fixme" => Some("Known issue requiring fix - prioritize resolution".into()),
910 "critical" => Some("Critical section - changes require extra review".into()),
911 "perf" => Some("Performance-sensitive code - benchmark any changes".into()),
912 "fn" | "function" => Some("Function implementation".into()),
913 "class" => Some("Class definition".into()),
914 "method" => Some("Method implementation".into()),
915 "purpose" => value.map(|v| v.trim_matches('"').to_string()),
916 _ => None,
917 }
918 }
919
920 pub fn parse_provenance(&self, lines: &[&str], start_idx: usize) -> Option<ProvenanceMarker> {
929 let mut marker = ProvenanceMarker::default();
930 let mut found_any = false;
931
932 for line in lines.iter().skip(start_idx) {
933 let line = *line;
934
935 if let Some(cap) = SOURCE_PATTERN.captures(line) {
937 if let Ok(origin) = cap.get(1).unwrap().as_str().parse() {
938 marker.source = origin;
939 found_any = true;
940 }
941 }
942
943 if let Some(cap) = CONFIDENCE_PATTERN.captures(line) {
945 if let Ok(conf) = cap.get(1).unwrap().as_str().parse::<f64>() {
946 marker.confidence = Some(conf.clamp(0.0, 1.0));
948 found_any = true;
949 }
950 }
951
952 if let Some(cap) = REVIEWED_PATTERN.captures(line) {
954 marker.reviewed = Some(cap.get(1).unwrap().as_str() == "true");
955 found_any = true;
956 }
957
958 if let Some(cap) = ID_PATTERN.captures(line) {
960 marker.generation_id = Some(cap.get(1).unwrap().as_str().to_string());
961 found_any = true;
962 }
963
964 let trimmed = line.trim();
966 let is_comment = trimmed.starts_with("//")
967 || trimmed.starts_with('*')
968 || trimmed.starts_with('#')
969 || trimmed.starts_with("/*");
970
971 if !is_comment {
972 break;
973 }
974
975 if line.contains("@acp:") && !line.contains("@acp:source") {
977 break;
978 }
979 }
980
981 if found_any {
982 Some(marker)
983 } else {
984 None
985 }
986 }
987
988 pub fn parse_annotations_with_provenance(
993 &self,
994 content: &str,
995 ) -> Vec<AnnotationWithProvenance> {
996 let annotations = self.parse_annotations(content);
997 let lines: Vec<&str> = content.lines().collect();
998
999 annotations
1000 .into_iter()
1001 .map(|ann| {
1002 let provenance = if ann.line < lines.len() {
1005 self.parse_provenance(&lines, ann.line)
1006 } else {
1007 None
1008 };
1009
1010 AnnotationWithProvenance {
1011 annotation: ann,
1012 provenance,
1013 }
1014 })
1015 .collect()
1016 }
1017}
1018
1019impl Default for Parser {
1020 fn default() -> Self {
1021 Self::new()
1022 }
1023}
1024
1025#[derive(Debug, Clone, Serialize, Deserialize)]
1028pub struct Annotation {
1029 pub name: String,
1031 pub value: Option<String>,
1033 pub directive: Option<String>,
1035 #[serde(default, skip_serializing_if = "is_false")]
1037 pub auto_generated: bool,
1038 pub line: usize,
1040}
1041
1042fn is_false(b: &bool) -> bool {
1043 !*b
1044}
1045
1046struct SymbolBuilder {
1048 name: String,
1049 qualified_name: String,
1050 line: usize,
1051 summary: Option<String>,
1052 purpose: Option<String>,
1053 calls: Vec<String>,
1054 symbol_type: SymbolType,
1055 behavioral: BehavioralAnnotations,
1057 lifecycle: LifecycleAnnotations,
1058 documentation: DocumentationAnnotations,
1059 performance: PerformanceAnnotations,
1060 type_info: TypeInfo,
1062}
1063
1064impl SymbolBuilder {
1065 fn new(name: String, line: usize, file_path: &str) -> Self {
1066 let qualified_name = format!("{}:{}", file_path, name);
1067 Self {
1068 name,
1069 qualified_name,
1070 line,
1071 summary: None,
1072 purpose: None,
1073 calls: vec![],
1074 symbol_type: SymbolType::Function,
1075 behavioral: BehavioralAnnotations::default(),
1077 lifecycle: LifecycleAnnotations::default(),
1078 documentation: DocumentationAnnotations::default(),
1079 performance: PerformanceAnnotations::default(),
1080 type_info: TypeInfo::default(),
1082 }
1083 }
1084
1085 fn build(self, file_path: &str) -> SymbolEntry {
1086 SymbolEntry {
1087 name: self.name,
1088 qualified_name: self.qualified_name,
1089 symbol_type: self.symbol_type,
1090 file: file_path.to_string(),
1091 lines: [self.line, self.line + 10], exported: true,
1093 signature: None,
1094 summary: self.summary,
1095 purpose: self.purpose,
1096 async_fn: self.behavioral.r#async, visibility: Visibility::Public,
1098 calls: self.calls,
1099 called_by: vec![], git: None,
1101 constraints: None,
1102 annotations: std::collections::HashMap::new(), behavioral: if self.behavioral.is_empty() {
1105 None
1106 } else {
1107 Some(self.behavioral)
1108 },
1109 lifecycle: if self.lifecycle.is_empty() {
1110 None
1111 } else {
1112 Some(self.lifecycle)
1113 },
1114 documentation: if self.documentation.is_empty() {
1115 None
1116 } else {
1117 Some(self.documentation)
1118 },
1119 performance: if self.performance.is_empty() {
1120 None
1121 } else {
1122 Some(self.performance)
1123 },
1124 type_info: if self.type_info.is_empty() {
1126 None
1127 } else {
1128 Some(self.type_info)
1129 },
1130 }
1131 }
1132}
1133
1134#[cfg(test)]
1139mod type_annotation_tests {
1140 use super::*;
1141 use std::io::Write;
1142 use tempfile::NamedTempFile;
1143
1144 fn parse_test_file(content: &str) -> ParseResult {
1145 let mut file = NamedTempFile::with_suffix(".ts").unwrap();
1146 write!(file, "{}", content).unwrap();
1147 let parser = Parser::new();
1148 parser.parse(file.path()).unwrap()
1149 }
1150
1151 #[test]
1152 fn test_param_with_type() {
1153 let content = r#"
1154// @acp:fn "test" - Test function
1155// @acp:param {string} name - User name
1156// @acp:param {number} age - User age
1157"#;
1158 let result = parse_test_file(content);
1159 assert_eq!(result.symbols.len(), 1);
1160
1161 let sym = &result.symbols[0];
1162 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1163 assert_eq!(type_info.params.len(), 2);
1164
1165 assert_eq!(type_info.params[0].name, "name");
1166 assert_eq!(type_info.params[0].r#type, Some("string".to_string()));
1167 assert_eq!(type_info.params[0].directive, Some("User name".to_string()));
1168
1169 assert_eq!(type_info.params[1].name, "age");
1170 assert_eq!(type_info.params[1].r#type, Some("number".to_string()));
1171 }
1172
1173 #[test]
1174 fn test_param_optional() {
1175 let content = r#"
1176// @acp:fn "test" - Test function
1177// @acp:param {string} [name] - Optional name
1178"#;
1179 let result = parse_test_file(content);
1180 let sym = &result.symbols[0];
1181 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1182
1183 assert_eq!(type_info.params[0].name, "name");
1184 assert!(type_info.params[0].optional);
1185 }
1186
1187 #[test]
1188 fn test_param_with_default() {
1189 let content = r#"
1190// @acp:fn "test" - Test function
1191// @acp:param {number} [limit=10] - Limit with default
1192"#;
1193 let result = parse_test_file(content);
1194 let sym = &result.symbols[0];
1195 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1196
1197 assert_eq!(type_info.params[0].name, "limit");
1198 assert!(type_info.params[0].optional);
1199 assert_eq!(type_info.params[0].default, Some("10".to_string()));
1200 }
1201
1202 #[test]
1203 fn test_param_without_type() {
1204 let content = r#"
1205// @acp:fn "test" - Test function
1206// @acp:param name - Just a name param
1207"#;
1208 let result = parse_test_file(content);
1209 let sym = &result.symbols[0];
1210 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1211
1212 assert_eq!(type_info.params[0].name, "name");
1213 assert!(type_info.params[0].r#type.is_none());
1214 }
1215
1216 #[test]
1217 fn test_returns_with_type() {
1218 let content = r#"
1219// @acp:fn "test" - Test function
1220// @acp:returns {Promise<User>} - Returns user promise
1221"#;
1222 let result = parse_test_file(content);
1223 let sym = &result.symbols[0];
1224 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1225
1226 let returns = type_info.returns.as_ref().expect("Should have returns");
1227 assert_eq!(returns.r#type, Some("Promise<User>".to_string()));
1228 assert_eq!(returns.directive, Some("Returns user promise".to_string()));
1229 }
1230
1231 #[test]
1232 fn test_returns_without_type() {
1233 let content = r#"
1234// @acp:fn "test" - Test function
1235// @acp:returns - Returns nothing special
1236"#;
1237 let result = parse_test_file(content);
1238 let sym = &result.symbols[0];
1239 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1240
1241 let returns = type_info.returns.as_ref().expect("Should have returns");
1242 assert!(returns.r#type.is_none());
1243 }
1244
1245 #[test]
1246 fn test_template() {
1247 let content = r#"
1248// @acp:fn "test" - Test function
1249// @acp:template T - Type parameter
1250"#;
1251 let result = parse_test_file(content);
1252 let sym = &result.symbols[0];
1253 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1254
1255 assert_eq!(type_info.type_params.len(), 1);
1256 assert_eq!(type_info.type_params[0].name, "T");
1257 assert!(type_info.type_params[0].constraint.is_none());
1258 }
1259
1260 #[test]
1261 fn test_template_with_constraint() {
1262 let content = r#"
1263// @acp:fn "test" - Test function
1264// @acp:template T extends BaseEntity - Entity type
1265"#;
1266 let result = parse_test_file(content);
1267 let sym = &result.symbols[0];
1268 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1269
1270 assert_eq!(type_info.type_params[0].name, "T");
1271 assert_eq!(
1272 type_info.type_params[0].constraint,
1273 Some("BaseEntity".to_string())
1274 );
1275 assert_eq!(
1276 type_info.type_params[0].directive,
1277 Some("Entity type".to_string())
1278 );
1279 }
1280
1281 #[test]
1282 fn test_complex_type_expression() {
1283 let content = r#"
1284// @acp:fn "test" - Test function
1285// @acp:param {Map<string, User | null>} userMap - Complex type
1286// @acp:returns {Promise<Array<User>>} - Returns users
1287"#;
1288 let result = parse_test_file(content);
1289 let sym = &result.symbols[0];
1290 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1291
1292 assert_eq!(
1293 type_info.params[0].r#type,
1294 Some("Map<string, User | null>".to_string())
1295 );
1296
1297 let returns = type_info.returns.as_ref().unwrap();
1298 assert_eq!(returns.r#type, Some("Promise<Array<User>>".to_string()));
1299 }
1300
1301 #[test]
1302 fn test_backward_compat_no_types() {
1303 let content = r#"
1305// @acp:fn "test" - Test function
1306// @acp:param userId - User ID
1307// @acp:returns - User object or null
1308"#;
1309 let result = parse_test_file(content);
1310 let sym = &result.symbols[0];
1311 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1312
1313 assert_eq!(type_info.params[0].name, "userId");
1315 assert!(type_info.params[0].r#type.is_none());
1316 }
1317
1318 #[test]
1319 fn test_type_source_is_acp() {
1320 let content = r#"
1321// @acp:fn "test" - Test function
1322// @acp:param {string} name - Name param
1323// @acp:returns {void} - Returns nothing
1324"#;
1325 let result = parse_test_file(content);
1326 let sym = &result.symbols[0];
1327 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1328
1329 assert_eq!(type_info.params[0].type_source, Some(TypeSource::Acp));
1331 assert_eq!(
1332 type_info.returns.as_ref().unwrap().type_source,
1333 Some(TypeSource::Acp)
1334 );
1335 }
1336}