@voodocs/cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,238 @@
1
+ """@darkarts
2
+ ⊢parser:darkarts.annotations
3
+ ∂{re,typing,symbols}
4
+ ⚠{@darkarts∈docstrings,unicode-support}
5
+ ⊨{∀parse→structured-output,¬modify-src,handle-errors}
6
+ 🔒{read-only}
7
+ ⚡{O(n)|n=annotation-length}
8
+
9
+ DarkArts Annotation Parser
10
+
11
+ Parses @darkarts symbolic annotations from source code.
12
+ """
13
+
14
+ import re
15
+ from typing import Dict, List, Optional, Any
16
+ from dataclasses import dataclass, field
17
+
18
+ from .symbols import VOCABULARY, get_meta_symbols
19
+
20
+
21
+ @dataclass
22
+ class DarkArtsAnnotation:
23
+ """Parsed DarkArts annotation."""
24
+ module: Optional[str] = None # From ⊢
25
+ dependencies: List[str] = field(default_factory=list) # From ∂
26
+ assumptions: List[str] = field(default_factory=list) # From ⚠
27
+ invariants: List[str] = field(default_factory=list) # From ⊨
28
+ security: Optional[str] = None # From 🔒
29
+ performance: Optional[str] = None # From ⚡
30
+ complexity: Optional[str] = None # From 📊
31
+ objectives: Optional[str] = None # From 🎯
32
+ configuration: Optional[str] = None # From ⚙️
33
+ raw_text: str = "" # Original annotation text
34
+
35
+ def to_dict(self) -> Dict[str, Any]:
36
+ """Convert to dictionary."""
37
+ return {
38
+ 'module': self.module,
39
+ 'dependencies': self.dependencies,
40
+ 'assumptions': self.assumptions,
41
+ 'invariants': self.invariants,
42
+ 'security': self.security,
43
+ 'performance': self.performance,
44
+ 'complexity': self.complexity,
45
+ 'objectives': self.objectives,
46
+ 'configuration': self.configuration,
47
+ }
48
+
49
+
50
+ class DarkArtsParser:
51
+ """
52
+ Parser for DarkArts symbolic annotations.
53
+
54
+ Extracts and parses @darkarts annotations from source code.
55
+ """
56
+
57
+ # Pattern to match @darkarts annotations
58
+ PATTERN = r'"""@darkarts\s*(.*?)\s*"""'
59
+
60
+ # Meta symbol mappings
61
+ META_SYMBOLS = {
62
+ '⊢': 'module',
63
+ '∂': 'dependencies',
64
+ '⚠': 'assumptions',
65
+ '⊨': 'invariants',
66
+ '🔒': 'security',
67
+ '⚡': 'performance',
68
+ '📊': 'complexity',
69
+ '🎯': 'objectives',
70
+ '⚙️': 'configuration',
71
+ }
72
+
73
+ def __init__(self):
74
+ """Initialize the parser."""
75
+ self.meta_symbols = get_meta_symbols()
76
+
77
+ def parse(self, source_code: str) -> List[DarkArtsAnnotation]:
78
+ """
79
+ Parse all @darkarts annotations from source code.
80
+
81
+ Args:
82
+ source_code: Source code containing @darkarts annotations
83
+
84
+ Returns:
85
+ List of parsed annotations
86
+ """
87
+ annotations = []
88
+
89
+ # Find all @darkarts blocks
90
+ matches = re.finditer(self.PATTERN, source_code, re.DOTALL)
91
+
92
+ for match in matches:
93
+ annotation_text = match.group(1)
94
+ annotation = self.parse_annotation(annotation_text)
95
+ annotations.append(annotation)
96
+
97
+ return annotations
98
+
99
+ def parse_annotation(self, text: str) -> DarkArtsAnnotation:
100
+ """
101
+ Parse a single @darkarts annotation.
102
+
103
+ Args:
104
+ text: Annotation text (without @darkarts marker)
105
+
106
+ Returns:
107
+ Parsed annotation
108
+ """
109
+ annotation = DarkArtsAnnotation(raw_text=text)
110
+
111
+ # Split into lines
112
+ lines = text.strip().split('\n')
113
+
114
+ for line in lines:
115
+ line = line.strip()
116
+ if not line:
117
+ continue
118
+
119
+ # Check for meta symbols
120
+ for symbol, field_name in self.META_SYMBOLS.items():
121
+ if line.startswith(symbol):
122
+ # Extract content after symbol
123
+ content = line[len(symbol):].strip()
124
+ self._parse_field(annotation, field_name, content)
125
+ break
126
+
127
+ return annotation
128
+
129
+ def _parse_field(self, annotation: DarkArtsAnnotation, field_name: str, content: str):
130
+ """
131
+ Parse a field and update annotation.
132
+
133
+ Args:
134
+ annotation: Annotation to update
135
+ field_name: Name of field (module, dependencies, etc.)
136
+ content: Content to parse
137
+ """
138
+ if field_name == 'module':
139
+ annotation.module = content
140
+
141
+ elif field_name == 'dependencies':
142
+ annotation.dependencies = self._parse_set(content)
143
+
144
+ elif field_name == 'assumptions':
145
+ annotation.assumptions = self._parse_set(content)
146
+
147
+ elif field_name == 'invariants':
148
+ annotation.invariants = self._parse_set(content)
149
+
150
+ elif field_name == 'security':
151
+ annotation.security = content
152
+
153
+ elif field_name == 'performance':
154
+ annotation.performance = content
155
+
156
+ elif field_name == 'complexity':
157
+ annotation.complexity = content
158
+
159
+ elif field_name == 'objectives':
160
+ annotation.objectives = content
161
+
162
+ elif field_name == 'configuration':
163
+ annotation.configuration = content
164
+
165
+ def _parse_set(self, content: str) -> List[str]:
166
+ """
167
+ Parse a set notation {a,b,c} into a list.
168
+
169
+ Args:
170
+ content: Content like "{a,b,c}" or "a,b,c"
171
+
172
+ Returns:
173
+ List of items
174
+ """
175
+ # Remove braces if present
176
+ content = content.strip()
177
+ if content.startswith('{') and content.endswith('}'):
178
+ content = content[1:-1]
179
+
180
+ # Split by comma
181
+ items = []
182
+ current = []
183
+ depth = 0
184
+ in_string = False
185
+
186
+ for char in content:
187
+ if char == '{' and not in_string:
188
+ depth += 1
189
+ elif char == '}' and not in_string:
190
+ depth -= 1
191
+ elif char == '"':
192
+ in_string = not in_string
193
+ elif char == ',' and depth == 0 and not in_string:
194
+ item = ''.join(current).strip()
195
+ if item:
196
+ items.append(item)
197
+ current = []
198
+ continue
199
+
200
+ current.append(char)
201
+
202
+ # Add last item
203
+ if current:
204
+ item = ''.join(current).strip()
205
+ if item:
206
+ items.append(item)
207
+
208
+ return items
209
+
210
+ def parse_file(self, file_path: str) -> List[DarkArtsAnnotation]:
211
+ """
212
+ Parse @darkarts annotations from a file.
213
+
214
+ Args:
215
+ file_path: Path to source file
216
+
217
+ Returns:
218
+ List of parsed annotations
219
+ """
220
+ with open(file_path, 'r', encoding='utf-8') as f:
221
+ source_code = f.read()
222
+
223
+ return self.parse(source_code)
224
+
225
+
226
+ # Convenience function
227
+ def parse_darkarts(source_code: str) -> List[DarkArtsAnnotation]:
228
+ """
229
+ Parse @darkarts annotations from source code.
230
+
231
+ Args:
232
+ source_code: Source code containing @darkarts annotations
233
+
234
+ Returns:
235
+ List of parsed annotations
236
+ """
237
+ parser = DarkArtsParser()
238
+ return parser.parse(source_code)
@@ -1,4 +1,11 @@
1
- """
1
+ """@darkarts
2
+ ⊢parser:annotations.multi-lang
3
+ ∂{re,ast,pathlib,types}
4
+ ⚠{src:utf8,@voodocs∈docstrings,yaml-lists,fs:readable}
5
+ ⊨{∀parse→¬modify-src,∀read→handle-encoding,parsed∈pyobj,dedup-invariants,lang-detect:accurate}
6
+ 🔒{read-only,¬exec}
7
+ ⚡{O(n'*m/c)|n'=files-with-annotations,m=avg-file-size,c=cache-constant,speedup=5-10x}
8
+
2
9
  DarkArts Annotation Parser
3
10
 
4
11
  Extracts DarkArts annotations from source code in multiple languages.
@@ -8,8 +15,10 @@ import re
8
15
  import ast
9
16
  import json
10
17
  import subprocess
18
+ import os
11
19
  from typing import List, Optional, Dict, Any
12
20
  from pathlib import Path
21
+ from functools import lru_cache
13
22
 
14
23
  from .types import (
15
24
  ParsedAnnotations,
@@ -39,6 +48,11 @@ class AnnotationParser:
39
48
  # Regex patterns for different languages
40
49
  PATTERNS = {
41
50
  Language.PYTHON: r'"""@voodocs\s*(.*?)\s*"""',
51
+ }
52
+
53
+ # DarkArts patterns (symbolic annotations)
54
+ DARKARTS_PATTERNS = {
55
+ Language.PYTHON: r'"""@darkarts\s*(.*?)\s*"""',
42
56
  Language.TYPESCRIPT: r'/\*@voodocs\s*(.*?)\s*\*/',
43
57
  Language.JAVASCRIPT: r'/\*@voodocs\s*(.*?)\s*\*/',
44
58
  Language.JAVA: r'/\*@voodocs\s*(.*?)\s*\*/',
@@ -75,7 +89,35 @@ class AnnotationParser:
75
89
  return mapping.get(ext, Language.PYTHON)
76
90
 
77
91
  def parse_file(self, source_file: str) -> ParsedAnnotations:
78
- """Parse annotations from a source file."""
92
+ """
93
+ Parse annotations from a source file.
94
+
95
+ Phase 1 Optimization: Cache results based on file modification time.
96
+ """
97
+ # Get file modification time for cache key
98
+ try:
99
+ mtime = os.path.getmtime(source_file)
100
+ except OSError:
101
+ mtime = 0
102
+
103
+ # Use cached version if available
104
+ return self._parse_file_cached(source_file, mtime)
105
+
106
+ @lru_cache(maxsize=1000)
107
+ def _parse_file_cached(self, source_file: str, mtime: float) -> ParsedAnnotations:
108
+ """
109
+ Parse annotations from a source file with caching.
110
+
111
+ Phase 1 Optimization: LRU cache with mtime as key.
112
+ Cache invalidates when file is modified (mtime changes).
113
+
114
+ Args:
115
+ source_file: Path to the source file
116
+ mtime: File modification time (used as cache key)
117
+
118
+ Returns:
119
+ ParsedAnnotations object
120
+ """
79
121
  with open(source_file, 'r', encoding='utf-8') as f:
80
122
  source_code = f.read()
81
123
 
@@ -92,9 +134,22 @@ class AnnotationParser:
92
134
  if language is None:
93
135
  language = self.detect_language(source_file)
94
136
 
95
- # Extract all annotation blocks
96
- pattern = self.PATTERNS[language]
97
- matches = re.finditer(pattern, source_code, re.DOTALL)
137
+ # Extract all annotation blocks (VooDocs and DarkArts)
138
+ pattern = self.PATTERNS.get(language, self.PATTERNS[Language.PYTHON])
139
+ darkarts_pattern = self.DARKARTS_PATTERNS.get(language, self.DARKARTS_PATTERNS[Language.PYTHON])
140
+
141
+ # Try VooDocs first
142
+ voodocs_matches = list(re.finditer(pattern, source_code, re.DOTALL))
143
+
144
+ # Also check for DarkArts
145
+ darkarts_matches = list(re.finditer(darkarts_pattern, source_code, re.DOTALL))
146
+
147
+ # If we found DarkArts annotations, translate them to VooDocs format
148
+ if darkarts_matches and not voodocs_matches:
149
+ return self._parse_darkarts_annotations(source_code, source_file, language, darkarts_matches)
150
+
151
+ # Use VooDocs annotations
152
+ matches = voodocs_matches
98
153
 
99
154
  annotations = []
100
155
  for match in matches:
@@ -628,10 +683,35 @@ class AnnotationParser:
628
683
  if 'side_effects' in annotations:
629
684
  func.side_effects = annotations['side_effects'] if isinstance(annotations['side_effects'], list) else [annotations['side_effects']]
630
685
 
686
+ @staticmethod
687
+ def _has_annotations(file_path: Path) -> bool:
688
+ """
689
+ Quick check if a file contains @voodocs annotations.
690
+
691
+ Phase 1 Optimization: Pre-filter files before full parsing.
692
+ Reads only first 10KB to check for @voodocs marker.
693
+
694
+ Args:
695
+ file_path: Path to the file to check
696
+
697
+ Returns:
698
+ True if file likely contains annotations, False otherwise
699
+ """
700
+ try:
701
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
702
+ # Read first 10KB (enough for module-level annotations)
703
+ preview = f.read(10000)
704
+ return '@voodocs' in preview
705
+ except Exception:
706
+ # If we can't read it, skip it
707
+ return False
708
+
631
709
  def parse_directory(self, directory: Path) -> List[ParsedAnnotations]:
632
710
  """
633
711
  Parse all source files in a directory recursively.
634
712
 
713
+ Phase 1 Optimization: Pre-filter files before parsing.
714
+
635
715
  Args:
636
716
  directory: Path to the directory to scan
637
717
 
@@ -662,6 +742,10 @@ class AnnotationParser:
662
742
  if path.suffix.lower() not in extensions:
663
743
  continue
664
744
 
745
+ # Phase 1 Optimization: Pre-filter files without annotations
746
+ if not self._has_annotations(path):
747
+ continue
748
+
665
749
  try:
666
750
  # Parse the file
667
751
  parsed = self.parse_file(str(path))
@@ -680,3 +764,100 @@ class AnnotationParser:
680
764
  continue
681
765
 
682
766
  return results
767
+
768
+ def _parse_darkarts_annotations(
769
+ self,
770
+ source_code: str,
771
+ source_file: str,
772
+ language: Language,
773
+ darkarts_matches: List
774
+ ) -> ParsedAnnotations:
775
+ """Parse @darkarts annotations and translate to VooDocs format."""
776
+ from .darkarts_parser import parse_darkarts
777
+ from .translator import DarkArtsTranslator
778
+
779
+ # Parse all @darkarts annotations
780
+ translator = DarkArtsTranslator()
781
+ translated_annotations = []
782
+
783
+ for match in darkarts_matches:
784
+ darkarts_text = match.group(1)
785
+
786
+ # Parse @darkarts annotation (parse_annotation expects just the content)
787
+ from .darkarts_parser import DarkArtsParser
788
+ parser = DarkArtsParser()
789
+ darkarts_annotation = parser.parse_annotation(darkarts_text)
790
+
791
+ # Process the annotation
792
+ if darkarts_annotation:
793
+ # Convert DarkArtsAnnotation to dict for YAML generation
794
+ annotation_dict = {
795
+ 'module': darkarts_annotation.module,
796
+ 'dependencies': darkarts_annotation.dependencies,
797
+ 'assumptions': darkarts_annotation.assumptions,
798
+ 'invariants': darkarts_annotation.invariants,
799
+ 'security_model': darkarts_annotation.security,
800
+ 'performance': darkarts_annotation.performance,
801
+ }
802
+
803
+ # Convert to @voodocs YAML format
804
+ voodocs_yaml = self._darkarts_to_voodocs_yaml(annotation_dict)
805
+
806
+ line_number = source_code[:match.start()].count('\n') + 1
807
+ translated_annotations.append({
808
+ 'text': voodocs_yaml,
809
+ 'line': line_number,
810
+ 'start': match.start(),
811
+ 'end': match.end(),
812
+ })
813
+
814
+ # Parse as if they were @voodocs
815
+ if language == Language.PYTHON:
816
+ return self._parse_python(source_code, source_file, translated_annotations)
817
+ else:
818
+ return self._parse_generic(source_code, source_file, language, translated_annotations)
819
+
820
+ def _darkarts_to_voodocs_yaml(self, annotation_dict: Dict[str, Any]) -> str:
821
+ """Convert DarkArts annotation dict to @voodocs YAML format."""
822
+ lines = []
823
+
824
+ # Module name becomes module_purpose (just use the identifier)
825
+ if annotation_dict.get('module'):
826
+ lines.append(f'module_purpose: "Module {annotation_dict["module"]}"')
827
+
828
+ # Dependencies - keep as-is
829
+ if annotation_dict.get('dependencies'):
830
+ lines.append('dependencies: [')
831
+ for dep in annotation_dict['dependencies']:
832
+ lines.append(f' "{dep}",')
833
+ lines.append(']')
834
+
835
+ # Assumptions - keep as-is
836
+ if annotation_dict.get('assumptions'):
837
+ lines.append('assumptions: [')
838
+ for assumption in annotation_dict['assumptions']:
839
+ lines.append(f' "{assumption}",')
840
+ lines.append(']')
841
+
842
+ # Invariants - keep as-is
843
+ if annotation_dict.get('invariants'):
844
+ lines.append('invariants: [')
845
+ for invariant in annotation_dict['invariants']:
846
+ lines.append(f' "{invariant}",')
847
+ lines.append(']')
848
+
849
+ # Security model
850
+ if annotation_dict.get('security_model'):
851
+ sec = annotation_dict['security_model']
852
+ if isinstance(sec, list):
853
+ sec = ', '.join(sec)
854
+ lines.append(f'security_model: "{sec}"')
855
+
856
+ # Performance
857
+ if annotation_dict.get('performance'):
858
+ perf = annotation_dict['performance']
859
+ if isinstance(perf, dict):
860
+ perf = str(perf)
861
+ lines.append(f'performance_model: "{perf}"')
862
+
863
+ return '\n'.join(lines)