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 imported_by: Vec::new(), module: module_name,
785 summary: file_summary,
786 purpose: purpose.clone(),
787 owner: owner.clone(),
788 inline: inline_annotations.clone(),
789 domains,
790 layer,
791 stability: None,
792 ai_hints: ai_hints.clone(),
793 git: None,
794 annotations: std::collections::HashMap::new(), bridge: crate::cache::BridgeMetadata::default(), version: file_version,
798 since: file_since,
799 license: file_license,
800 author: file_author,
801 lifecycle: if file_lifecycle.is_empty() {
802 None
803 } else {
804 Some(file_lifecycle)
805 },
806 refs: Vec::new(),
808 style: None,
809 };
810
811 Ok(ParseResult {
812 file,
813 symbols,
814 calls,
815 lock_level,
816 lock_directive,
817 ai_hints,
818 hacks,
819 inline_annotations,
820 purpose,
821 owner,
822 })
823 }
824
825 pub fn parse_annotations(&self, content: &str) -> Vec<Annotation> {
828 let mut annotations = Vec::new();
829 let lines: Vec<&str> = content.lines().collect();
830 let mut i = 0;
831
832 while i < lines.len() {
833 let line = lines[i];
834 let line_1indexed = i + 1;
835
836 for cap in ANNOTATION_PATTERN.captures_iter(line) {
837 let name = cap.get(1).unwrap().as_str().to_string();
838 let value = cap.get(2).map(|m| m.as_str().trim().to_string());
839 let mut directive = cap.get(3).map(|m| m.as_str().trim().to_string());
840
841 let mut j = i + 1;
843 while j < lines.len() {
844 if let Some(cont_cap) = CONTINUATION_PATTERN.captures(lines[j]) {
845 let continuation = cont_cap.get(1).unwrap().as_str().trim();
846 if let Some(ref mut dir) = directive {
847 dir.push(' ');
848 dir.push_str(continuation);
849 } else {
850 directive = Some(continuation.to_string());
851 }
852 j += 1;
853 } else {
854 break;
855 }
856 }
857
858 let (final_directive, auto_generated) = match directive {
860 Some(d) if !d.is_empty() => (Some(d), false),
861 _ => (self.default_directive(&name, value.as_deref()), true),
862 };
863
864 annotations.push(Annotation {
865 name,
866 value,
867 directive: final_directive,
868 auto_generated,
869 line: line_1indexed,
870 });
871 }
872
873 i += 1;
874 }
875
876 annotations
877 }
878
879 fn default_directive(&self, name: &str, value: Option<&str>) -> Option<String> {
882 match name {
883 "lock" => match value {
884 Some("frozen") => Some("MUST NOT modify this code under any circumstances".into()),
885 Some("restricted") => {
886 Some("Explain proposed changes and wait for explicit approval".into())
887 }
888 Some("approval-required") => {
889 Some("Propose changes and request confirmation before applying".into())
890 }
891 Some("tests-required") => {
892 Some("All changes must include corresponding tests".into())
893 }
894 Some("docs-required") => Some("All changes must update documentation".into()),
895 Some("review-required") => {
896 Some("Changes require code review before merging".into())
897 }
898 Some("normal") | None => {
899 Some("Safe to modify following project conventions".into())
900 }
901 Some("experimental") => {
902 Some("Experimental code - changes welcome but may be unstable".into())
903 }
904 _ => None,
905 },
906 "ref" => value.map(|url| format!("Consult {} before making changes", url)),
907 "hack" => Some("Temporary workaround - check expiry before modifying".into()),
908 "deprecated" => Some("Do not use or extend - see replacement annotation".into()),
909 "todo" => Some("Pending work item - address before release".into()),
910 "fixme" => Some("Known issue requiring fix - prioritize resolution".into()),
911 "critical" => Some("Critical section - changes require extra review".into()),
912 "perf" => Some("Performance-sensitive code - benchmark any changes".into()),
913 "fn" | "function" => Some("Function implementation".into()),
914 "class" => Some("Class definition".into()),
915 "method" => Some("Method implementation".into()),
916 "purpose" => value.map(|v| v.trim_matches('"').to_string()),
917 _ => None,
918 }
919 }
920
921 pub fn parse_provenance(&self, lines: &[&str], start_idx: usize) -> Option<ProvenanceMarker> {
930 let mut marker = ProvenanceMarker::default();
931 let mut found_any = false;
932
933 for line in lines.iter().skip(start_idx) {
934 let line = *line;
935
936 if let Some(cap) = SOURCE_PATTERN.captures(line) {
938 if let Ok(origin) = cap.get(1).unwrap().as_str().parse() {
939 marker.source = origin;
940 found_any = true;
941 }
942 }
943
944 if let Some(cap) = CONFIDENCE_PATTERN.captures(line) {
946 if let Ok(conf) = cap.get(1).unwrap().as_str().parse::<f64>() {
947 marker.confidence = Some(conf.clamp(0.0, 1.0));
949 found_any = true;
950 }
951 }
952
953 if let Some(cap) = REVIEWED_PATTERN.captures(line) {
955 marker.reviewed = Some(cap.get(1).unwrap().as_str() == "true");
956 found_any = true;
957 }
958
959 if let Some(cap) = ID_PATTERN.captures(line) {
961 marker.generation_id = Some(cap.get(1).unwrap().as_str().to_string());
962 found_any = true;
963 }
964
965 let trimmed = line.trim();
967 let is_comment = trimmed.starts_with("//")
968 || trimmed.starts_with('*')
969 || trimmed.starts_with('#')
970 || trimmed.starts_with("/*");
971
972 if !is_comment {
973 break;
974 }
975
976 if line.contains("@acp:") && !line.contains("@acp:source") {
978 break;
979 }
980 }
981
982 if found_any {
983 Some(marker)
984 } else {
985 None
986 }
987 }
988
989 pub fn parse_annotations_with_provenance(
994 &self,
995 content: &str,
996 ) -> Vec<AnnotationWithProvenance> {
997 let annotations = self.parse_annotations(content);
998 let lines: Vec<&str> = content.lines().collect();
999
1000 annotations
1001 .into_iter()
1002 .map(|ann| {
1003 let provenance = if ann.line < lines.len() {
1006 self.parse_provenance(&lines, ann.line)
1007 } else {
1008 None
1009 };
1010
1011 AnnotationWithProvenance {
1012 annotation: ann,
1013 provenance,
1014 }
1015 })
1016 .collect()
1017 }
1018}
1019
1020impl Default for Parser {
1021 fn default() -> Self {
1022 Self::new()
1023 }
1024}
1025
1026#[derive(Debug, Clone, Serialize, Deserialize)]
1029pub struct Annotation {
1030 pub name: String,
1032 pub value: Option<String>,
1034 pub directive: Option<String>,
1036 #[serde(default, skip_serializing_if = "is_false")]
1038 pub auto_generated: bool,
1039 pub line: usize,
1041}
1042
1043fn is_false(b: &bool) -> bool {
1044 !*b
1045}
1046
1047struct SymbolBuilder {
1049 name: String,
1050 qualified_name: String,
1051 line: usize,
1052 summary: Option<String>,
1053 purpose: Option<String>,
1054 calls: Vec<String>,
1055 symbol_type: SymbolType,
1056 behavioral: BehavioralAnnotations,
1058 lifecycle: LifecycleAnnotations,
1059 documentation: DocumentationAnnotations,
1060 performance: PerformanceAnnotations,
1061 type_info: TypeInfo,
1063}
1064
1065impl SymbolBuilder {
1066 fn new(name: String, line: usize, file_path: &str) -> Self {
1067 let qualified_name = format!("{}:{}", file_path, name);
1068 Self {
1069 name,
1070 qualified_name,
1071 line,
1072 summary: None,
1073 purpose: None,
1074 calls: vec![],
1075 symbol_type: SymbolType::Function,
1076 behavioral: BehavioralAnnotations::default(),
1078 lifecycle: LifecycleAnnotations::default(),
1079 documentation: DocumentationAnnotations::default(),
1080 performance: PerformanceAnnotations::default(),
1081 type_info: TypeInfo::default(),
1083 }
1084 }
1085
1086 fn build(self, file_path: &str) -> SymbolEntry {
1087 SymbolEntry {
1088 name: self.name,
1089 qualified_name: self.qualified_name,
1090 symbol_type: self.symbol_type,
1091 file: file_path.to_string(),
1092 lines: [self.line, self.line + 10], exported: true,
1094 signature: None,
1095 summary: self.summary,
1096 purpose: self.purpose,
1097 async_fn: self.behavioral.r#async, visibility: Visibility::Public,
1099 calls: self.calls,
1100 called_by: vec![], git: None,
1102 constraints: None,
1103 annotations: std::collections::HashMap::new(), behavioral: if self.behavioral.is_empty() {
1106 None
1107 } else {
1108 Some(self.behavioral)
1109 },
1110 lifecycle: if self.lifecycle.is_empty() {
1111 None
1112 } else {
1113 Some(self.lifecycle)
1114 },
1115 documentation: if self.documentation.is_empty() {
1116 None
1117 } else {
1118 Some(self.documentation)
1119 },
1120 performance: if self.performance.is_empty() {
1121 None
1122 } else {
1123 Some(self.performance)
1124 },
1125 type_info: if self.type_info.is_empty() {
1127 None
1128 } else {
1129 Some(self.type_info)
1130 },
1131 }
1132 }
1133}
1134
1135#[cfg(test)]
1140mod type_annotation_tests {
1141 use super::*;
1142 use std::io::Write;
1143 use tempfile::NamedTempFile;
1144
1145 fn parse_test_file(content: &str) -> ParseResult {
1146 let mut file = NamedTempFile::with_suffix(".ts").unwrap();
1147 write!(file, "{}", content).unwrap();
1148 let parser = Parser::new();
1149 parser.parse(file.path()).unwrap()
1150 }
1151
1152 #[test]
1153 fn test_param_with_type() {
1154 let content = r#"
1155// @acp:fn "test" - Test function
1156// @acp:param {string} name - User name
1157// @acp:param {number} age - User age
1158"#;
1159 let result = parse_test_file(content);
1160 assert_eq!(result.symbols.len(), 1);
1161
1162 let sym = &result.symbols[0];
1163 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1164 assert_eq!(type_info.params.len(), 2);
1165
1166 assert_eq!(type_info.params[0].name, "name");
1167 assert_eq!(type_info.params[0].r#type, Some("string".to_string()));
1168 assert_eq!(type_info.params[0].directive, Some("User name".to_string()));
1169
1170 assert_eq!(type_info.params[1].name, "age");
1171 assert_eq!(type_info.params[1].r#type, Some("number".to_string()));
1172 }
1173
1174 #[test]
1175 fn test_param_optional() {
1176 let content = r#"
1177// @acp:fn "test" - Test function
1178// @acp:param {string} [name] - Optional name
1179"#;
1180 let result = parse_test_file(content);
1181 let sym = &result.symbols[0];
1182 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1183
1184 assert_eq!(type_info.params[0].name, "name");
1185 assert!(type_info.params[0].optional);
1186 }
1187
1188 #[test]
1189 fn test_param_with_default() {
1190 let content = r#"
1191// @acp:fn "test" - Test function
1192// @acp:param {number} [limit=10] - Limit with default
1193"#;
1194 let result = parse_test_file(content);
1195 let sym = &result.symbols[0];
1196 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1197
1198 assert_eq!(type_info.params[0].name, "limit");
1199 assert!(type_info.params[0].optional);
1200 assert_eq!(type_info.params[0].default, Some("10".to_string()));
1201 }
1202
1203 #[test]
1204 fn test_param_without_type() {
1205 let content = r#"
1206// @acp:fn "test" - Test function
1207// @acp:param name - Just a name param
1208"#;
1209 let result = parse_test_file(content);
1210 let sym = &result.symbols[0];
1211 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1212
1213 assert_eq!(type_info.params[0].name, "name");
1214 assert!(type_info.params[0].r#type.is_none());
1215 }
1216
1217 #[test]
1218 fn test_returns_with_type() {
1219 let content = r#"
1220// @acp:fn "test" - Test function
1221// @acp:returns {Promise<User>} - Returns user promise
1222"#;
1223 let result = parse_test_file(content);
1224 let sym = &result.symbols[0];
1225 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1226
1227 let returns = type_info.returns.as_ref().expect("Should have returns");
1228 assert_eq!(returns.r#type, Some("Promise<User>".to_string()));
1229 assert_eq!(returns.directive, Some("Returns user promise".to_string()));
1230 }
1231
1232 #[test]
1233 fn test_returns_without_type() {
1234 let content = r#"
1235// @acp:fn "test" - Test function
1236// @acp:returns - Returns nothing special
1237"#;
1238 let result = parse_test_file(content);
1239 let sym = &result.symbols[0];
1240 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1241
1242 let returns = type_info.returns.as_ref().expect("Should have returns");
1243 assert!(returns.r#type.is_none());
1244 }
1245
1246 #[test]
1247 fn test_template() {
1248 let content = r#"
1249// @acp:fn "test" - Test function
1250// @acp:template T - Type parameter
1251"#;
1252 let result = parse_test_file(content);
1253 let sym = &result.symbols[0];
1254 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1255
1256 assert_eq!(type_info.type_params.len(), 1);
1257 assert_eq!(type_info.type_params[0].name, "T");
1258 assert!(type_info.type_params[0].constraint.is_none());
1259 }
1260
1261 #[test]
1262 fn test_template_with_constraint() {
1263 let content = r#"
1264// @acp:fn "test" - Test function
1265// @acp:template T extends BaseEntity - Entity type
1266"#;
1267 let result = parse_test_file(content);
1268 let sym = &result.symbols[0];
1269 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1270
1271 assert_eq!(type_info.type_params[0].name, "T");
1272 assert_eq!(
1273 type_info.type_params[0].constraint,
1274 Some("BaseEntity".to_string())
1275 );
1276 assert_eq!(
1277 type_info.type_params[0].directive,
1278 Some("Entity type".to_string())
1279 );
1280 }
1281
1282 #[test]
1283 fn test_complex_type_expression() {
1284 let content = r#"
1285// @acp:fn "test" - Test function
1286// @acp:param {Map<string, User | null>} userMap - Complex type
1287// @acp:returns {Promise<Array<User>>} - Returns users
1288"#;
1289 let result = parse_test_file(content);
1290 let sym = &result.symbols[0];
1291 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1292
1293 assert_eq!(
1294 type_info.params[0].r#type,
1295 Some("Map<string, User | null>".to_string())
1296 );
1297
1298 let returns = type_info.returns.as_ref().unwrap();
1299 assert_eq!(returns.r#type, Some("Promise<Array<User>>".to_string()));
1300 }
1301
1302 #[test]
1303 fn test_backward_compat_no_types() {
1304 let content = r#"
1306// @acp:fn "test" - Test function
1307// @acp:param userId - User ID
1308// @acp:returns - User object or null
1309"#;
1310 let result = parse_test_file(content);
1311 let sym = &result.symbols[0];
1312 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1313
1314 assert_eq!(type_info.params[0].name, "userId");
1316 assert!(type_info.params[0].r#type.is_none());
1317 }
1318
1319 #[test]
1320 fn test_type_source_is_acp() {
1321 let content = r#"
1322// @acp:fn "test" - Test function
1323// @acp:param {string} name - Name param
1324// @acp:returns {void} - Returns nothing
1325"#;
1326 let result = parse_test_file(content);
1327 let sym = &result.symbols[0];
1328 let type_info = sym.type_info.as_ref().expect("Should have type_info");
1329
1330 assert_eq!(type_info.params[0].type_source, Some(TypeSource::Acp));
1332 assert_eq!(
1333 type_info.returns.as_ref().unwrap().type_source,
1334 Some(TypeSource::Acp)
1335 );
1336 }
1337}