@voodocs/cli 0.1.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.
- package/LICENSE +37 -0
- package/README.md +153 -0
- package/USAGE.md +314 -0
- package/cli.py +1340 -0
- package/examples/.cursorrules +437 -0
- package/examples/instructions/.claude/instructions.md +372 -0
- package/examples/instructions/.cursorrules +437 -0
- package/examples/instructions/.windsurfrules +437 -0
- package/examples/instructions/VOODOCS_INSTRUCTIONS.md +437 -0
- package/examples/math_example.py +41 -0
- package/examples/phase2_test.py +24 -0
- package/examples/test_compound_conditions.py +40 -0
- package/examples/test_math_example.py +186 -0
- package/lib/darkarts/README.md +115 -0
- package/lib/darkarts/__init__.py +16 -0
- package/lib/darkarts/annotations/__init__.py +34 -0
- package/lib/darkarts/annotations/parser.py +618 -0
- package/lib/darkarts/annotations/types.py +181 -0
- package/lib/darkarts/cli.py +128 -0
- package/lib/darkarts/core/__init__.py +32 -0
- package/lib/darkarts/core/interface.py +256 -0
- package/lib/darkarts/core/loader.py +231 -0
- package/lib/darkarts/core/plugin.py +215 -0
- package/lib/darkarts/core/registry.py +146 -0
- package/lib/darkarts/exceptions.py +51 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts +9 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js +69 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts +111 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js +365 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js.map +1 -0
- package/lib/darkarts/parsers/typescript/package-lock.json +51 -0
- package/lib/darkarts/parsers/typescript/package.json +19 -0
- package/lib/darkarts/parsers/typescript/src/cli.ts +41 -0
- package/lib/darkarts/parsers/typescript/src/parser.ts +408 -0
- package/lib/darkarts/parsers/typescript/tsconfig.json +19 -0
- package/lib/darkarts/plugins/voodocs/__init__.py +379 -0
- package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +151 -0
- package/lib/darkarts/plugins/voodocs/annotation_validator.py +280 -0
- package/lib/darkarts/plugins/voodocs/api_spec_generator.py +486 -0
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +610 -0
- package/lib/darkarts/plugins/voodocs/html_exporter.py +260 -0
- package/lib/darkarts/plugins/voodocs/instruction_generator.py +706 -0
- package/lib/darkarts/plugins/voodocs/pdf_exporter.py +66 -0
- package/lib/darkarts/plugins/voodocs/test_generator.py +636 -0
- package/package.json +70 -0
- package/requirements.txt +13 -0
- package/templates/ci/github-actions.yml +73 -0
- package/templates/ci/gitlab-ci.yml +35 -0
- package/templates/ci/pre-commit-hook.sh +26 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DarkArts Annotation Parser
|
|
3
|
+
|
|
4
|
+
Extracts DarkArts annotations from source code in multiple languages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import ast
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import List, Optional, Dict, Any
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .types import (
|
|
15
|
+
ParsedAnnotations,
|
|
16
|
+
ModuleAnnotation,
|
|
17
|
+
ClassAnnotation,
|
|
18
|
+
FunctionAnnotation,
|
|
19
|
+
ComplexityAnnotation,
|
|
20
|
+
ErrorCase,
|
|
21
|
+
StateTransition,
|
|
22
|
+
Language,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from darkarts.exceptions import ParserNotBuiltError
|
|
27
|
+
from darkarts.parser_checker import check_parser_for_language
|
|
28
|
+
except ImportError:
|
|
29
|
+
# Fallback if exceptions module not available
|
|
30
|
+
class ParserNotBuiltError(Exception):
|
|
31
|
+
pass
|
|
32
|
+
def check_parser_for_language(lang):
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AnnotationParser:
|
|
37
|
+
"""Parser for DarkArts annotations."""
|
|
38
|
+
|
|
39
|
+
# Regex patterns for different languages
|
|
40
|
+
PATTERNS = {
|
|
41
|
+
Language.PYTHON: r'"""@voodocs\s*(.*?)\s*"""',
|
|
42
|
+
Language.TYPESCRIPT: r'/\*@voodocs\s*(.*?)\s*\*/',
|
|
43
|
+
Language.JAVASCRIPT: r'/\*@voodocs\s*(.*?)\s*\*/',
|
|
44
|
+
Language.JAVA: r'/\*@voodocs\s*(.*?)\s*\*/',
|
|
45
|
+
Language.CPP: r'/\*@voodocs\s*(.*?)\s*\*/',
|
|
46
|
+
Language.CSHARP: r'/\*@voodocs\s*(.*?)\s*\*/',
|
|
47
|
+
Language.GO: r'/\*@voodocs\s*(.*?)\s*\*/',
|
|
48
|
+
Language.RUST: r'/\*@voodocs\s*(.*?)\s*\*/',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def detect_language(self, source_file: str) -> Language:
|
|
55
|
+
"""Detect programming language from file extension."""
|
|
56
|
+
ext = Path(source_file).suffix.lower()
|
|
57
|
+
|
|
58
|
+
mapping = {
|
|
59
|
+
'.py': Language.PYTHON,
|
|
60
|
+
'.ts': Language.TYPESCRIPT,
|
|
61
|
+
'.tsx': Language.TYPESCRIPT,
|
|
62
|
+
'.js': Language.JAVASCRIPT,
|
|
63
|
+
'.jsx': Language.JAVASCRIPT,
|
|
64
|
+
'.java': Language.JAVA,
|
|
65
|
+
'.cpp': Language.CPP,
|
|
66
|
+
'.cc': Language.CPP,
|
|
67
|
+
'.cxx': Language.CPP,
|
|
68
|
+
'.h': Language.CPP,
|
|
69
|
+
'.hpp': Language.CPP,
|
|
70
|
+
'.cs': Language.CSHARP,
|
|
71
|
+
'.go': Language.GO,
|
|
72
|
+
'.rs': Language.RUST,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return mapping.get(ext, Language.PYTHON)
|
|
76
|
+
|
|
77
|
+
def parse_file(self, source_file: str) -> ParsedAnnotations:
|
|
78
|
+
"""Parse annotations from a source file."""
|
|
79
|
+
with open(source_file, 'r', encoding='utf-8') as f:
|
|
80
|
+
source_code = f.read()
|
|
81
|
+
|
|
82
|
+
language = self.detect_language(source_file)
|
|
83
|
+
return self.parse_source(source_code, source_file, language)
|
|
84
|
+
|
|
85
|
+
def parse_source(
|
|
86
|
+
self,
|
|
87
|
+
source_code: str,
|
|
88
|
+
source_file: str = "<string>",
|
|
89
|
+
language: Optional[Language] = None
|
|
90
|
+
) -> ParsedAnnotations:
|
|
91
|
+
"""Parse annotations from source code."""
|
|
92
|
+
if language is None:
|
|
93
|
+
language = self.detect_language(source_file)
|
|
94
|
+
|
|
95
|
+
# Extract all annotation blocks
|
|
96
|
+
pattern = self.PATTERNS[language]
|
|
97
|
+
matches = re.finditer(pattern, source_code, re.DOTALL)
|
|
98
|
+
|
|
99
|
+
annotations = []
|
|
100
|
+
for match in matches:
|
|
101
|
+
annotation_text = match.group(1)
|
|
102
|
+
line_number = source_code[:match.start()].count('\n') + 1
|
|
103
|
+
annotations.append({
|
|
104
|
+
'text': annotation_text,
|
|
105
|
+
'line': line_number,
|
|
106
|
+
'start': match.start(),
|
|
107
|
+
'end': match.end(),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# Parse based on language
|
|
111
|
+
if language == Language.PYTHON:
|
|
112
|
+
return self._parse_python(source_code, source_file, annotations)
|
|
113
|
+
elif language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
|
|
114
|
+
return self._parse_typescript(source_file)
|
|
115
|
+
else:
|
|
116
|
+
return self._parse_generic(source_code, source_file, language, annotations)
|
|
117
|
+
|
|
118
|
+
def _parse_python(
|
|
119
|
+
self,
|
|
120
|
+
source_code: str,
|
|
121
|
+
source_file: str,
|
|
122
|
+
annotations: List[Dict]
|
|
123
|
+
) -> ParsedAnnotations:
|
|
124
|
+
"""Parse Python source code with AST."""
|
|
125
|
+
try:
|
|
126
|
+
tree = ast.parse(source_code)
|
|
127
|
+
except SyntaxError:
|
|
128
|
+
# Fallback to generic parsing
|
|
129
|
+
return self._parse_generic(source_code, source_file, Language.PYTHON, annotations)
|
|
130
|
+
|
|
131
|
+
module = ModuleAnnotation(
|
|
132
|
+
name=Path(source_file).stem,
|
|
133
|
+
source_file=source_file,
|
|
134
|
+
language=Language.PYTHON,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Map annotations to AST nodes
|
|
138
|
+
annotation_map = self._build_annotation_map(source_code, annotations)
|
|
139
|
+
|
|
140
|
+
# Parse module-level annotations (first few lines)
|
|
141
|
+
for line in range(1, 20):
|
|
142
|
+
if line in annotation_map:
|
|
143
|
+
parsed = self._parse_annotation_text(annotation_map[line])
|
|
144
|
+
if 'module_purpose' in parsed or 'dependencies' in parsed or 'assumptions' in parsed:
|
|
145
|
+
self._parse_module_annotation(annotation_map[line], module)
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
# Parse classes and functions
|
|
149
|
+
for node in ast.walk(tree):
|
|
150
|
+
if isinstance(node, ast.ClassDef):
|
|
151
|
+
cls_annotation = self._parse_class_node(node, annotation_map, source_file)
|
|
152
|
+
if cls_annotation:
|
|
153
|
+
module.classes.append(cls_annotation)
|
|
154
|
+
|
|
155
|
+
elif isinstance(node, ast.FunctionDef):
|
|
156
|
+
# Only top-level functions (not methods)
|
|
157
|
+
if isinstance(node, ast.FunctionDef) and node.col_offset == 0:
|
|
158
|
+
func_annotation = self._parse_function_node(node, annotation_map, source_file)
|
|
159
|
+
if func_annotation:
|
|
160
|
+
module.functions.append(func_annotation)
|
|
161
|
+
|
|
162
|
+
return ParsedAnnotations(
|
|
163
|
+
module=module,
|
|
164
|
+
language=Language.PYTHON,
|
|
165
|
+
source_file=source_file,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _build_annotation_map(self, source_code: str, annotations: List[Dict]) -> Dict[int, str]:
|
|
169
|
+
"""Build a map from line numbers to annotation text."""
|
|
170
|
+
annotation_map = {}
|
|
171
|
+
for ann in annotations:
|
|
172
|
+
annotation_map[ann['line']] = ann['text']
|
|
173
|
+
return annotation_map
|
|
174
|
+
|
|
175
|
+
def _parse_class_node(
|
|
176
|
+
self,
|
|
177
|
+
node: ast.ClassDef,
|
|
178
|
+
annotation_map: Dict[int, str],
|
|
179
|
+
source_file: str
|
|
180
|
+
) -> Optional[ClassAnnotation]:
|
|
181
|
+
"""Parse a class node."""
|
|
182
|
+
# Look for annotation in docstring (after class definition)
|
|
183
|
+
class_line = node.lineno
|
|
184
|
+
annotation_text = None
|
|
185
|
+
|
|
186
|
+
# Check a few lines after the class definition (docstring)
|
|
187
|
+
for offset in range(1, 10):
|
|
188
|
+
if class_line + offset in annotation_map:
|
|
189
|
+
annotation_text = annotation_map[class_line + offset]
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
if not annotation_text:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
cls_annotation = ClassAnnotation(
|
|
196
|
+
name=node.name,
|
|
197
|
+
line_number=class_line,
|
|
198
|
+
source_file=source_file,
|
|
199
|
+
raw_annotation=annotation_text,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Parse class annotation
|
|
203
|
+
parsed = self._parse_annotation_text(annotation_text)
|
|
204
|
+
|
|
205
|
+
if 'class_invariants' in parsed:
|
|
206
|
+
cls_annotation.class_invariants = parsed['class_invariants']
|
|
207
|
+
|
|
208
|
+
if 'state_transitions' in parsed:
|
|
209
|
+
cls_annotation.state_transitions = [
|
|
210
|
+
self._parse_state_transition(st)
|
|
211
|
+
for st in parsed['state_transitions']
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
# Parse methods
|
|
215
|
+
for item in node.body:
|
|
216
|
+
if isinstance(item, ast.FunctionDef):
|
|
217
|
+
method_annotation = self._parse_function_node(item, annotation_map, source_file)
|
|
218
|
+
if method_annotation:
|
|
219
|
+
cls_annotation.methods.append(method_annotation)
|
|
220
|
+
|
|
221
|
+
return cls_annotation
|
|
222
|
+
|
|
223
|
+
def _parse_function_node(
|
|
224
|
+
self,
|
|
225
|
+
node: ast.FunctionDef,
|
|
226
|
+
annotation_map: Dict[int, str],
|
|
227
|
+
source_file: str
|
|
228
|
+
) -> Optional[FunctionAnnotation]:
|
|
229
|
+
"""Parse a function node."""
|
|
230
|
+
# Look for annotation in docstring (after function definition)
|
|
231
|
+
func_line = node.lineno
|
|
232
|
+
annotation_text = None
|
|
233
|
+
|
|
234
|
+
# Check a few lines after the function definition (docstring)
|
|
235
|
+
for offset in range(1, 10):
|
|
236
|
+
if func_line + offset in annotation_map:
|
|
237
|
+
annotation_text = annotation_map[func_line + offset]
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
if not annotation_text:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
func_annotation = FunctionAnnotation(
|
|
244
|
+
name=node.name,
|
|
245
|
+
line_number=func_line,
|
|
246
|
+
source_file=source_file,
|
|
247
|
+
raw_annotation=annotation_text,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Parse function annotation
|
|
251
|
+
parsed = self._parse_annotation_text(annotation_text)
|
|
252
|
+
|
|
253
|
+
if 'solve' in parsed:
|
|
254
|
+
func_annotation.solve = parsed['solve']
|
|
255
|
+
|
|
256
|
+
if 'preconditions' in parsed:
|
|
257
|
+
func_annotation.preconditions = parsed['preconditions']
|
|
258
|
+
|
|
259
|
+
if 'postconditions' in parsed:
|
|
260
|
+
func_annotation.postconditions = parsed['postconditions']
|
|
261
|
+
|
|
262
|
+
if 'invariants' in parsed:
|
|
263
|
+
func_annotation.invariants = parsed['invariants']
|
|
264
|
+
|
|
265
|
+
if 'optimize' in parsed:
|
|
266
|
+
func_annotation.optimize = parsed['optimize']
|
|
267
|
+
|
|
268
|
+
if 'complexity' in parsed:
|
|
269
|
+
func_annotation.complexity = self._parse_complexity(parsed['complexity'])
|
|
270
|
+
|
|
271
|
+
if 'error_cases' in parsed:
|
|
272
|
+
func_annotation.error_cases = [
|
|
273
|
+
self._parse_error_case(ec)
|
|
274
|
+
for ec in parsed['error_cases']
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
return func_annotation
|
|
278
|
+
|
|
279
|
+
def _parse_module_annotation(self, annotation_text: str, module: ModuleAnnotation):
|
|
280
|
+
"""Parse module-level annotation."""
|
|
281
|
+
parsed = self._parse_annotation_text(annotation_text)
|
|
282
|
+
|
|
283
|
+
if 'module_purpose' in parsed:
|
|
284
|
+
module.module_purpose = parsed['module_purpose']
|
|
285
|
+
|
|
286
|
+
if 'dependencies' in parsed:
|
|
287
|
+
module.dependencies = parsed['dependencies']
|
|
288
|
+
|
|
289
|
+
if 'assumptions' in parsed:
|
|
290
|
+
module.assumptions = parsed['assumptions']
|
|
291
|
+
|
|
292
|
+
def _parse_annotation_text(self, text: str) -> Dict[str, Any]:
|
|
293
|
+
"""Parse annotation text into structured data."""
|
|
294
|
+
result = {}
|
|
295
|
+
|
|
296
|
+
# Simple key-value parsing
|
|
297
|
+
lines = text.strip().split('\n')
|
|
298
|
+
current_key = None
|
|
299
|
+
current_value = []
|
|
300
|
+
|
|
301
|
+
for line in lines:
|
|
302
|
+
line = line.strip()
|
|
303
|
+
if not line:
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# Check if this is a key line
|
|
307
|
+
if ':' in line and not line.startswith('"') and not line.startswith('['):
|
|
308
|
+
# Save previous key-value
|
|
309
|
+
if current_key:
|
|
310
|
+
result[current_key] = self._parse_value(current_key, current_value)
|
|
311
|
+
|
|
312
|
+
# Start new key
|
|
313
|
+
key, value = line.split(':', 1)
|
|
314
|
+
current_key = key.strip()
|
|
315
|
+
current_value = [value.strip()]
|
|
316
|
+
else:
|
|
317
|
+
# Continuation of current value
|
|
318
|
+
if current_key:
|
|
319
|
+
current_value.append(line)
|
|
320
|
+
|
|
321
|
+
# Save last key-value
|
|
322
|
+
if current_key:
|
|
323
|
+
result[current_key] = self._parse_value(current_key, current_value)
|
|
324
|
+
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
def _parse_value(self, key: str, value_lines: List[str]) -> Any:
|
|
328
|
+
"""Parse a value based on its key."""
|
|
329
|
+
text = ' '.join(value_lines).strip()
|
|
330
|
+
|
|
331
|
+
# Remove quotes
|
|
332
|
+
if text.startswith('"') and text.endswith('"'):
|
|
333
|
+
return text[1:-1]
|
|
334
|
+
|
|
335
|
+
# Parse lists
|
|
336
|
+
if text.startswith('['):
|
|
337
|
+
return self._parse_list(text)
|
|
338
|
+
|
|
339
|
+
# Parse objects
|
|
340
|
+
if text.startswith('{'):
|
|
341
|
+
return self._parse_object(text)
|
|
342
|
+
|
|
343
|
+
return text
|
|
344
|
+
|
|
345
|
+
def _parse_list(self, text: str) -> List[str]:
|
|
346
|
+
"""Parse a list value."""
|
|
347
|
+
# Remove brackets
|
|
348
|
+
text = text.strip('[]').strip()
|
|
349
|
+
|
|
350
|
+
# Split by commas (simple approach)
|
|
351
|
+
items = []
|
|
352
|
+
current = []
|
|
353
|
+
depth = 0
|
|
354
|
+
in_string = False
|
|
355
|
+
|
|
356
|
+
for char in text:
|
|
357
|
+
if char == '"' and (not current or current[-1] != '\\'):
|
|
358
|
+
in_string = not in_string
|
|
359
|
+
elif char in '{[' and not in_string:
|
|
360
|
+
depth += 1
|
|
361
|
+
elif char in '}]' and not in_string:
|
|
362
|
+
depth -= 1
|
|
363
|
+
elif char == ',' and depth == 0 and not in_string:
|
|
364
|
+
items.append(''.join(current).strip().strip('"'))
|
|
365
|
+
current = []
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
current.append(char)
|
|
369
|
+
|
|
370
|
+
if current:
|
|
371
|
+
items.append(''.join(current).strip().strip('"'))
|
|
372
|
+
|
|
373
|
+
return items
|
|
374
|
+
|
|
375
|
+
def _parse_object(self, text: str) -> Dict[str, Any]:
|
|
376
|
+
"""Parse an object value."""
|
|
377
|
+
# Simple object parsing
|
|
378
|
+
result = {}
|
|
379
|
+
text = text.strip('{}').strip()
|
|
380
|
+
|
|
381
|
+
# Split by commas
|
|
382
|
+
pairs = text.split(',')
|
|
383
|
+
for pair in pairs:
|
|
384
|
+
if ':' in pair:
|
|
385
|
+
key, value = pair.split(':', 1)
|
|
386
|
+
result[key.strip().strip('"')] = value.strip().strip('"')
|
|
387
|
+
|
|
388
|
+
return result
|
|
389
|
+
|
|
390
|
+
def _parse_complexity(self, data: Any) -> ComplexityAnnotation:
|
|
391
|
+
"""Parse complexity annotation."""
|
|
392
|
+
if isinstance(data, dict):
|
|
393
|
+
return ComplexityAnnotation(
|
|
394
|
+
time=data.get('time', 'O(1)'),
|
|
395
|
+
space=data.get('space', 'O(1)'),
|
|
396
|
+
best_case=data.get('best_case'),
|
|
397
|
+
worst_case=data.get('worst_case'),
|
|
398
|
+
average_case=data.get('average_case'),
|
|
399
|
+
)
|
|
400
|
+
return ComplexityAnnotation()
|
|
401
|
+
|
|
402
|
+
def _parse_error_case(self, text: str) -> ErrorCase:
|
|
403
|
+
"""Parse error case annotation."""
|
|
404
|
+
# Format: "condition → ErrorType"
|
|
405
|
+
if '→' in text:
|
|
406
|
+
condition, error_type = text.split('→', 1)
|
|
407
|
+
return ErrorCase(
|
|
408
|
+
condition=condition.strip(),
|
|
409
|
+
error_type=error_type.strip(),
|
|
410
|
+
)
|
|
411
|
+
return ErrorCase(condition=text, error_type="Error")
|
|
412
|
+
|
|
413
|
+
def _parse_state_transition(self, text: str) -> StateTransition:
|
|
414
|
+
"""Parse state transition annotation."""
|
|
415
|
+
# Format: "FROM → TO: condition" or "FROM → TO"
|
|
416
|
+
if ':' in text:
|
|
417
|
+
transition, condition = text.split(':', 1)
|
|
418
|
+
condition = condition.strip()
|
|
419
|
+
else:
|
|
420
|
+
transition = text
|
|
421
|
+
condition = None
|
|
422
|
+
|
|
423
|
+
if '→' in transition:
|
|
424
|
+
from_state, to_state = transition.split('→', 1)
|
|
425
|
+
return StateTransition(
|
|
426
|
+
from_state=from_state.strip(),
|
|
427
|
+
to_state=to_state.strip(),
|
|
428
|
+
condition=condition,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return StateTransition(from_state="UNKNOWN", to_state="UNKNOWN")
|
|
432
|
+
|
|
433
|
+
def _parse_generic(
|
|
434
|
+
self,
|
|
435
|
+
source_code: str,
|
|
436
|
+
source_file: str,
|
|
437
|
+
language: Language,
|
|
438
|
+
annotations: List[Dict]
|
|
439
|
+
) -> ParsedAnnotations:
|
|
440
|
+
"""Generic parsing for non-Python languages."""
|
|
441
|
+
module = ModuleAnnotation(
|
|
442
|
+
name=Path(source_file).stem,
|
|
443
|
+
source_file=source_file,
|
|
444
|
+
language=language,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Parse each annotation
|
|
448
|
+
for ann in annotations:
|
|
449
|
+
parsed = self._parse_annotation_text(ann['text'])
|
|
450
|
+
|
|
451
|
+
# Determine annotation type based on content
|
|
452
|
+
if 'module_purpose' in parsed or 'dependencies' in parsed:
|
|
453
|
+
self._parse_module_annotation(ann['text'], module)
|
|
454
|
+
elif 'class_invariants' in parsed:
|
|
455
|
+
# Class annotation
|
|
456
|
+
cls = ClassAnnotation(
|
|
457
|
+
name="UnknownClass",
|
|
458
|
+
line_number=ann['line'],
|
|
459
|
+
source_file=source_file,
|
|
460
|
+
raw_annotation=ann['text'],
|
|
461
|
+
)
|
|
462
|
+
if 'class_invariants' in parsed:
|
|
463
|
+
cls.class_invariants = parsed['class_invariants']
|
|
464
|
+
if 'state_transitions' in parsed:
|
|
465
|
+
cls.state_transitions = [
|
|
466
|
+
self._parse_state_transition(st)
|
|
467
|
+
for st in parsed['state_transitions']
|
|
468
|
+
]
|
|
469
|
+
module.classes.append(cls)
|
|
470
|
+
else:
|
|
471
|
+
# Function annotation
|
|
472
|
+
func = FunctionAnnotation(
|
|
473
|
+
name="UnknownFunction",
|
|
474
|
+
line_number=ann['line'],
|
|
475
|
+
source_file=source_file,
|
|
476
|
+
raw_annotation=ann['text'],
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if 'solve' in parsed:
|
|
480
|
+
func.solve = parsed['solve']
|
|
481
|
+
if 'preconditions' in parsed:
|
|
482
|
+
func.preconditions = parsed['preconditions']
|
|
483
|
+
if 'postconditions' in parsed:
|
|
484
|
+
func.postconditions = parsed['postconditions']
|
|
485
|
+
if 'invariants' in parsed:
|
|
486
|
+
func.invariants = parsed['invariants']
|
|
487
|
+
if 'optimize' in parsed:
|
|
488
|
+
func.optimize = parsed['optimize']
|
|
489
|
+
if 'complexity' in parsed:
|
|
490
|
+
func.complexity = self._parse_complexity(parsed['complexity'])
|
|
491
|
+
if 'error_cases' in parsed:
|
|
492
|
+
func.error_cases = [
|
|
493
|
+
self._parse_error_case(ec)
|
|
494
|
+
for ec in parsed['error_cases']
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
module.functions.append(func)
|
|
498
|
+
|
|
499
|
+
return ParsedAnnotations(
|
|
500
|
+
module=module,
|
|
501
|
+
language=language,
|
|
502
|
+
source_file=source_file,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
def _parse_typescript(self, source_file: str) -> ParsedAnnotations:
|
|
506
|
+
"""Parse TypeScript/JavaScript source code using the TypeScript parser."""
|
|
507
|
+
# Check if TypeScript parser is built
|
|
508
|
+
try:
|
|
509
|
+
check_parser_for_language('typescript')
|
|
510
|
+
except ParserNotBuiltError:
|
|
511
|
+
raise
|
|
512
|
+
|
|
513
|
+
# Get the path to the TypeScript parser CLI
|
|
514
|
+
parser_dir = Path(__file__).parent.parent / 'parsers' / 'typescript'
|
|
515
|
+
parser_cli = parser_dir / 'dist' / 'cli.js'
|
|
516
|
+
|
|
517
|
+
if not parser_cli.exists():
|
|
518
|
+
raise ParserNotBuiltError("TypeScript")
|
|
519
|
+
|
|
520
|
+
# Run the TypeScript parser
|
|
521
|
+
try:
|
|
522
|
+
result = subprocess.run(
|
|
523
|
+
['node', str(parser_cli), source_file],
|
|
524
|
+
capture_output=True,
|
|
525
|
+
text=True,
|
|
526
|
+
check=True
|
|
527
|
+
)
|
|
528
|
+
data = json.loads(result.stdout)
|
|
529
|
+
except subprocess.CalledProcessError as e:
|
|
530
|
+
raise RuntimeError(f"TypeScript parser failed: {e.stderr}")
|
|
531
|
+
except json.JSONDecodeError as e:
|
|
532
|
+
raise RuntimeError(f"Failed to parse TypeScript parser output: {e}")
|
|
533
|
+
|
|
534
|
+
# Convert the TypeScript parser output to our internal format
|
|
535
|
+
language = Language.TYPESCRIPT if data['language'] == 'typescript' else Language.JAVASCRIPT
|
|
536
|
+
|
|
537
|
+
module = ModuleAnnotation(
|
|
538
|
+
name=Path(source_file).stem,
|
|
539
|
+
source_file=source_file,
|
|
540
|
+
language=language,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Parse module annotations
|
|
544
|
+
if data.get('module_annotations'):
|
|
545
|
+
self._apply_module_annotations(module, data['module_annotations'])
|
|
546
|
+
|
|
547
|
+
# Parse classes
|
|
548
|
+
for cls_data in data.get('classes', []):
|
|
549
|
+
cls = ClassAnnotation(
|
|
550
|
+
name=cls_data['name'],
|
|
551
|
+
line_number=cls_data['line'],
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if cls_data.get('annotations'):
|
|
555
|
+
self._apply_class_annotations(cls, cls_data['annotations'])
|
|
556
|
+
|
|
557
|
+
# Parse methods
|
|
558
|
+
for method_data in cls_data.get('methods', []):
|
|
559
|
+
method = FunctionAnnotation(
|
|
560
|
+
name=method_data['name'],
|
|
561
|
+
line_number=method_data['line'],
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
if method_data.get('annotations'):
|
|
565
|
+
self._apply_function_annotations(method, method_data['annotations'])
|
|
566
|
+
|
|
567
|
+
cls.methods.append(method)
|
|
568
|
+
|
|
569
|
+
module.classes.append(cls)
|
|
570
|
+
|
|
571
|
+
# Parse functions
|
|
572
|
+
for func_data in data.get('functions', []):
|
|
573
|
+
func = FunctionAnnotation(
|
|
574
|
+
name=func_data['name'],
|
|
575
|
+
line_number=func_data['line'],
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if func_data.get('annotations'):
|
|
579
|
+
self._apply_function_annotations(func, func_data['annotations'])
|
|
580
|
+
|
|
581
|
+
module.functions.append(func)
|
|
582
|
+
|
|
583
|
+
return ParsedAnnotations(
|
|
584
|
+
module=module,
|
|
585
|
+
language=language,
|
|
586
|
+
source_file=source_file,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
def _apply_module_annotations(self, module: ModuleAnnotation, annotations: Dict[str, Any]):
|
|
590
|
+
"""Apply annotations to a module."""
|
|
591
|
+
if 'module_purpose' in annotations:
|
|
592
|
+
module.purpose = annotations['module_purpose']
|
|
593
|
+
if 'dependencies' in annotations:
|
|
594
|
+
module.dependencies = annotations['dependencies'] if isinstance(annotations['dependencies'], list) else [annotations['dependencies']]
|
|
595
|
+
if 'assumptions' in annotations:
|
|
596
|
+
module.assumptions = annotations['assumptions'] if isinstance(annotations['assumptions'], list) else [annotations['assumptions']]
|
|
597
|
+
|
|
598
|
+
def _apply_class_annotations(self, cls: ClassAnnotation, annotations: Dict[str, Any]):
|
|
599
|
+
"""Apply annotations to a class."""
|
|
600
|
+
if 'class_invariants' in annotations:
|
|
601
|
+
cls.invariants = annotations['class_invariants'] if isinstance(annotations['class_invariants'], list) else [annotations['class_invariants']]
|
|
602
|
+
if 'state_transitions' in annotations:
|
|
603
|
+
transitions = annotations['state_transitions']
|
|
604
|
+
if isinstance(transitions, list):
|
|
605
|
+
cls.state_transitions = [self._parse_state_transition(t) for t in transitions]
|
|
606
|
+
|
|
607
|
+
def _apply_function_annotations(self, func: FunctionAnnotation, annotations: Dict[str, Any]):
|
|
608
|
+
"""Apply annotations to a function."""
|
|
609
|
+
if 'preconditions' in annotations:
|
|
610
|
+
func.preconditions = annotations['preconditions'] if isinstance(annotations['preconditions'], list) else [annotations['preconditions']]
|
|
611
|
+
if 'postconditions' in annotations:
|
|
612
|
+
func.postconditions = annotations['postconditions'] if isinstance(annotations['postconditions'], list) else [annotations['postconditions']]
|
|
613
|
+
if 'invariants' in annotations:
|
|
614
|
+
func.invariants = annotations['invariants'] if isinstance(annotations['invariants'], list) else [annotations['invariants']]
|
|
615
|
+
if 'complexity' in annotations:
|
|
616
|
+
func.complexity = self._parse_complexity(annotations['complexity'])
|
|
617
|
+
if 'side_effects' in annotations:
|
|
618
|
+
func.side_effects = annotations['side_effects'] if isinstance(annotations['side_effects'], list) else [annotations['side_effects']]
|