@voodocs/cli 2.4.0 → 2.5.1
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/CHANGELOG.md +47 -0
- package/README.md +26 -0
- package/lib/cli/analyze.py +277 -0
- package/lib/darkarts/priority_analyzer/__init__.py +0 -0
- package/lib/darkarts/priority_analyzer/analyzer.py +301 -0
- package/lib/darkarts/priority_analyzer/complexity.py +271 -0
- package/lib/darkarts/priority_analyzer/dependencies.py +275 -0
- package/lib/darkarts/priority_analyzer/security.py +200 -0
- package/lib/darkarts/voodocs_lite_dict.py +216 -0
- package/lib/darkarts/voodocs_lite_dict_v2.py +198 -0
- package/lib/darkarts/voodocs_lite_parser.py +343 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [2.5.1] - 2024-12-24
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Packaging Issue**: Fixed npm package to include missing files from v2.5.0
|
|
7
|
+
- Added `lib/darkarts/priority_analyzer/` directory
|
|
8
|
+
- Added `lib/darkarts/voodocs_lite_dict.py`
|
|
9
|
+
- Added `lib/darkarts/voodocs_lite_dict_v2.py`
|
|
10
|
+
- Added `lib/darkarts/voodocs_lite_parser.py`
|
|
11
|
+
- v2.5.0 was published with CHANGELOG updates but missing actual code files
|
|
12
|
+
- All Priority Analyzer and VooDocs Lite features now properly included
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## [2.5.0] - 2024-12-24
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Priority Analyzer** - New `voodocs analyze` command to identify which files need annotations most urgently
|
|
20
|
+
- **Complexity Analysis**: LOC, cyclomatic complexity, function count
|
|
21
|
+
- **Security Keyword Detection**: auth, password, admin, crypto, SQL, payment, etc.
|
|
22
|
+
- **Dependency Analysis**: Import tracking and dependent file detection
|
|
23
|
+
- **Weighted Scoring**: Complexity (30%), Security (40%), Dependencies (20%), Annotations (10%)
|
|
24
|
+
- **Priority Levels**: CRITICAL (80-100), HIGH (60-79), MEDIUM (40-59), LOW (20-39), MINIMAL (0-19)
|
|
25
|
+
- **Multiple Output Formats**: text, JSON, CSV, Markdown
|
|
26
|
+
- **Filtering Options**: By priority level, minimum score, exclude patterns
|
|
27
|
+
- **Smart Suggestions**: Context-aware recommendations for each file
|
|
28
|
+
- **Priority Analyzer Modules**:
|
|
29
|
+
- `lib/darkarts/priority_analyzer/complexity.py` - Complexity analysis engine
|
|
30
|
+
- `lib/darkarts/priority_analyzer/security.py` - Security keyword detector
|
|
31
|
+
- `lib/darkarts/priority_analyzer/dependencies.py` - Dependency tracker
|
|
32
|
+
- `lib/darkarts/priority_analyzer/analyzer.py` - Main priority analyzer
|
|
33
|
+
- `lib/cli/analyze.py` - CLI command implementation
|
|
34
|
+
- **Comprehensive Test Suite** - 16 unit tests for priority analyzer functionality
|
|
35
|
+
|
|
36
|
+
### Improved
|
|
37
|
+
- Heuristics for identifying security-sensitive code
|
|
38
|
+
- Dependency tracking for TypeScript, JavaScript, Python, and Solidity
|
|
39
|
+
- Annotation coverage detection and penalty calculation
|
|
40
|
+
- Priority scoring algorithm with empirical validation
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
- Added Priority Analyzer section to README.md
|
|
44
|
+
- Added analyze command documentation with examples
|
|
45
|
+
- Added priority level definitions and scoring factors
|
|
46
|
+
- Added specification document for priority analyzer
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
3
50
|
## [2.4.0] - 2025-12-24
|
|
4
51
|
|
|
5
52
|
### Added
|
package/README.md
CHANGED
|
@@ -348,6 +348,32 @@ voodocs convert src/ --to lite --dry-run # Preview changes
|
|
|
348
348
|
voodocs convert src/ --to lite --in-place # Modify files in-place
|
|
349
349
|
```
|
|
350
350
|
|
|
351
|
+
### `voodocs analyze`
|
|
352
|
+
|
|
353
|
+
**NEW in v2.5.0!** Analyze files and suggest annotation priorities based on complexity, security, and dependencies:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
voodocs analyze src/ # Analyze directory
|
|
357
|
+
voodocs analyze src/ --top 20 # Show top 20 files
|
|
358
|
+
voodocs analyze src/ --priority critical # Filter by priority
|
|
359
|
+
voodocs analyze src/ --format json # JSON output
|
|
360
|
+
voodocs analyze src/ --min-score 60 # Only show files with score >= 60
|
|
361
|
+
voodocs analyze src/ --exclude node_modules,dist # Exclude patterns
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Priority Levels:**
|
|
365
|
+
- 🔴 **CRITICAL** (80-100): Highly complex, security-sensitive, or widely-used files
|
|
366
|
+
- 🟠 **HIGH** (60-79): Complex or security-sensitive files
|
|
367
|
+
- 🟡 **MEDIUM** (40-59): Moderately complex files
|
|
368
|
+
- 🟢 **LOW** (20-39): Simple files with some complexity
|
|
369
|
+
- ⚪ **MINIMAL** (0-19): Very simple files
|
|
370
|
+
|
|
371
|
+
**Scoring Factors:**
|
|
372
|
+
- **Complexity** (30%): Lines of code, cyclomatic complexity, function count
|
|
373
|
+
- **Security** (40%): Security-sensitive keywords (auth, password, admin, etc.)
|
|
374
|
+
- **Dependencies** (20%): Import count and dependent files
|
|
375
|
+
- **Annotations** (10%): Penalty for missing VooDocs annotations
|
|
376
|
+
|
|
351
377
|
### `voodocs generate`
|
|
352
378
|
|
|
353
379
|
Generate documentation from annotations:
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI command for VooDocs Priority Analyzer
|
|
3
|
+
|
|
4
|
+
Usage: voodocs analyze [path] [options]
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Add parent directory to path for imports
|
|
12
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
|
13
|
+
|
|
14
|
+
from lib.darkarts.priority_analyzer.analyzer import PriorityAnalyzer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_analyze(args):
|
|
18
|
+
"""Execute the analyze command."""
|
|
19
|
+
# Get path (default to current directory)
|
|
20
|
+
path = args.path if hasattr(args, 'path') and args.path else '.'
|
|
21
|
+
path = os.path.abspath(path)
|
|
22
|
+
|
|
23
|
+
# Check if path exists
|
|
24
|
+
if not os.path.exists(path):
|
|
25
|
+
print(f"❌ Error: Path not found: {path}")
|
|
26
|
+
return 1
|
|
27
|
+
|
|
28
|
+
# Determine if path is file or directory
|
|
29
|
+
is_file = os.path.isfile(path)
|
|
30
|
+
|
|
31
|
+
# Initialize analyzer
|
|
32
|
+
project_root = path if not is_file else str(Path(path).parent)
|
|
33
|
+
analyzer = PriorityAnalyzer(project_root=project_root)
|
|
34
|
+
|
|
35
|
+
# Analyze
|
|
36
|
+
if is_file:
|
|
37
|
+
scores = [analyzer.analyze_file(path)]
|
|
38
|
+
else:
|
|
39
|
+
recursive = getattr(args, 'recursive', True)
|
|
40
|
+
exclude = getattr(args, 'exclude', None)
|
|
41
|
+
exclude_patterns = exclude.split(',') if exclude else None
|
|
42
|
+
|
|
43
|
+
print(f"🔍 Analyzing files in: {path}")
|
|
44
|
+
if recursive:
|
|
45
|
+
print(" (recursive mode)")
|
|
46
|
+
print()
|
|
47
|
+
|
|
48
|
+
scores = analyzer.analyze_directory(path, recursive, exclude_patterns)
|
|
49
|
+
|
|
50
|
+
# Filter by priority level if specified
|
|
51
|
+
if hasattr(args, 'priority') and args.priority:
|
|
52
|
+
priority_filter = args.priority.upper()
|
|
53
|
+
scores = [s for s in scores if s.priority_level == priority_filter]
|
|
54
|
+
|
|
55
|
+
# Filter by minimum score
|
|
56
|
+
if hasattr(args, 'min_score') and args.min_score:
|
|
57
|
+
scores = [s for s in scores if s.priority_score >= args.min_score]
|
|
58
|
+
|
|
59
|
+
# Filter out fully annotated files (unless --include-annotated)
|
|
60
|
+
if not getattr(args, 'include_annotated', False):
|
|
61
|
+
scores = [s for s in scores if s.annotation_coverage < 1.0]
|
|
62
|
+
|
|
63
|
+
# Limit to top N
|
|
64
|
+
top_n = getattr(args, 'top', 10)
|
|
65
|
+
if top_n and top_n > 0:
|
|
66
|
+
scores = scores[:top_n]
|
|
67
|
+
|
|
68
|
+
# Output format
|
|
69
|
+
output_format = getattr(args, 'format', 'text')
|
|
70
|
+
|
|
71
|
+
if output_format == 'json':
|
|
72
|
+
_output_json(scores)
|
|
73
|
+
elif output_format == 'csv':
|
|
74
|
+
_output_csv(scores)
|
|
75
|
+
elif output_format == 'markdown':
|
|
76
|
+
_output_markdown(scores)
|
|
77
|
+
else:
|
|
78
|
+
_output_text(scores, path)
|
|
79
|
+
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _output_text(scores, path):
|
|
84
|
+
"""Output results in human-readable text format."""
|
|
85
|
+
if not scores:
|
|
86
|
+
print("✅ No files need annotations!")
|
|
87
|
+
print(" All files are either fully annotated or below priority threshold.")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Calculate statistics
|
|
91
|
+
total_files = len(scores)
|
|
92
|
+
priority_dist = {
|
|
93
|
+
'CRITICAL': len([s for s in scores if s.priority_level == 'CRITICAL']),
|
|
94
|
+
'HIGH': len([s for s in scores if s.priority_level == 'HIGH']),
|
|
95
|
+
'MEDIUM': len([s for s in scores if s.priority_level == 'MEDIUM']),
|
|
96
|
+
'LOW': len([s for s in scores if s.priority_level == 'LOW']),
|
|
97
|
+
'MINIMAL': len([s for s in scores if s.priority_level == 'MINIMAL']),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
annotated_count = sum(1 for s in scores if s.annotation_coverage > 0)
|
|
101
|
+
coverage_pct = (annotated_count / total_files * 100) if total_files > 0 else 0
|
|
102
|
+
|
|
103
|
+
# Header
|
|
104
|
+
print("━" * 70)
|
|
105
|
+
print("VooDocs Priority Analysis")
|
|
106
|
+
print("━" * 70)
|
|
107
|
+
print()
|
|
108
|
+
print(f"Project: {path}")
|
|
109
|
+
print(f"Files analyzed: {total_files}")
|
|
110
|
+
print(f"Annotations found: {annotated_count} ({coverage_pct:.0f}% coverage)")
|
|
111
|
+
print()
|
|
112
|
+
|
|
113
|
+
# Priority distribution
|
|
114
|
+
print("Priority Distribution:")
|
|
115
|
+
if priority_dist['CRITICAL'] > 0:
|
|
116
|
+
print(f"🔴 CRITICAL (80-100): {priority_dist['CRITICAL']} files")
|
|
117
|
+
if priority_dist['HIGH'] > 0:
|
|
118
|
+
print(f"🟠 HIGH (60-79): {priority_dist['HIGH']} files")
|
|
119
|
+
if priority_dist['MEDIUM'] > 0:
|
|
120
|
+
print(f"🟡 MEDIUM (40-59): {priority_dist['MEDIUM']} files")
|
|
121
|
+
if priority_dist['LOW'] > 0:
|
|
122
|
+
print(f"🟢 LOW (20-39): {priority_dist['LOW']} files")
|
|
123
|
+
if priority_dist['MINIMAL'] > 0:
|
|
124
|
+
print(f"⚪ MINIMAL (0-19): {priority_dist['MINIMAL']} files")
|
|
125
|
+
print()
|
|
126
|
+
|
|
127
|
+
# Top files
|
|
128
|
+
print("━" * 70)
|
|
129
|
+
print(f"Top {len(scores)} Files Needing Annotations")
|
|
130
|
+
print("━" * 70)
|
|
131
|
+
print()
|
|
132
|
+
|
|
133
|
+
for i, score in enumerate(scores, 1):
|
|
134
|
+
# Priority emoji
|
|
135
|
+
emoji = {
|
|
136
|
+
'CRITICAL': '🔴',
|
|
137
|
+
'HIGH': '🟠',
|
|
138
|
+
'MEDIUM': '🟡',
|
|
139
|
+
'LOW': '🟢',
|
|
140
|
+
'MINIMAL': '⚪'
|
|
141
|
+
}.get(score.priority_level, '⚪')
|
|
142
|
+
|
|
143
|
+
# Relative path
|
|
144
|
+
rel_path = os.path.relpath(score.filepath, path) if not os.path.isfile(path) else score.filepath
|
|
145
|
+
|
|
146
|
+
print(f"{i}. {emoji} {rel_path} (Score: {score.priority_score:.0f})")
|
|
147
|
+
print(f" Complexity: {score.complexity_score} Security: {score.security_score} Dependencies: {score.dependency_score} Annotations: {score.annotation_penalty}")
|
|
148
|
+
print()
|
|
149
|
+
|
|
150
|
+
# Reasons
|
|
151
|
+
if score.reasons:
|
|
152
|
+
print(" Reasons:")
|
|
153
|
+
for reason in score.reasons:
|
|
154
|
+
print(f" • {reason}")
|
|
155
|
+
print()
|
|
156
|
+
|
|
157
|
+
# Suggestions
|
|
158
|
+
if score.suggestions:
|
|
159
|
+
print(" Suggestions:")
|
|
160
|
+
for suggestion in score.suggestions:
|
|
161
|
+
print(f" ✓ {suggestion}")
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
# Footer
|
|
165
|
+
print("━" * 70)
|
|
166
|
+
print()
|
|
167
|
+
print("💡 Tip: Start with CRITICAL files first. Use:")
|
|
168
|
+
if scores:
|
|
169
|
+
first_file = scores[0].filepath
|
|
170
|
+
print(f" voodocs companion {first_file}")
|
|
171
|
+
print()
|
|
172
|
+
print(" Or convert to VooDocs Lite for faster annotation:")
|
|
173
|
+
print(f" voodocs convert {path} --to lite --in-place")
|
|
174
|
+
print()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _output_json(scores):
|
|
178
|
+
"""Output results in JSON format."""
|
|
179
|
+
import json
|
|
180
|
+
|
|
181
|
+
data = []
|
|
182
|
+
for score in scores:
|
|
183
|
+
data.append({
|
|
184
|
+
'filepath': score.filepath,
|
|
185
|
+
'priority_score': score.priority_score,
|
|
186
|
+
'priority_level': score.priority_level,
|
|
187
|
+
'complexity_score': score.complexity_score,
|
|
188
|
+
'security_score': score.security_score,
|
|
189
|
+
'dependency_score': score.dependency_score,
|
|
190
|
+
'annotation_penalty': score.annotation_penalty,
|
|
191
|
+
'annotation_coverage': score.annotation_coverage,
|
|
192
|
+
'loc': score.loc,
|
|
193
|
+
'functions': score.functions,
|
|
194
|
+
'security_keywords': score.security_keywords,
|
|
195
|
+
'dependent_count': score.dependent_count,
|
|
196
|
+
'reasons': score.reasons,
|
|
197
|
+
'suggestions': score.suggestions
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
print(json.dumps(data, indent=2))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _output_csv(scores):
|
|
204
|
+
"""Output results in CSV format."""
|
|
205
|
+
import csv
|
|
206
|
+
import sys
|
|
207
|
+
|
|
208
|
+
writer = csv.writer(sys.stdout)
|
|
209
|
+
|
|
210
|
+
# Header
|
|
211
|
+
writer.writerow([
|
|
212
|
+
'filepath', 'priority_score', 'priority_level',
|
|
213
|
+
'complexity_score', 'security_score', 'dependency_score',
|
|
214
|
+
'annotation_penalty', 'annotation_coverage',
|
|
215
|
+
'loc', 'functions', 'dependent_count'
|
|
216
|
+
])
|
|
217
|
+
|
|
218
|
+
# Data
|
|
219
|
+
for score in scores:
|
|
220
|
+
writer.writerow([
|
|
221
|
+
score.filepath,
|
|
222
|
+
score.priority_score,
|
|
223
|
+
score.priority_level,
|
|
224
|
+
score.complexity_score,
|
|
225
|
+
score.security_score,
|
|
226
|
+
score.dependency_score,
|
|
227
|
+
score.annotation_penalty,
|
|
228
|
+
score.annotation_coverage,
|
|
229
|
+
score.loc,
|
|
230
|
+
score.functions,
|
|
231
|
+
score.dependent_count
|
|
232
|
+
])
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _output_markdown(scores):
|
|
236
|
+
"""Output results in Markdown format."""
|
|
237
|
+
print("# VooDocs Priority Analysis")
|
|
238
|
+
print()
|
|
239
|
+
print("## Summary")
|
|
240
|
+
print()
|
|
241
|
+
print(f"- **Files analyzed:** {len(scores)}")
|
|
242
|
+
print()
|
|
243
|
+
print("## Top Files")
|
|
244
|
+
print()
|
|
245
|
+
|
|
246
|
+
for i, score in enumerate(scores, 1):
|
|
247
|
+
emoji = {
|
|
248
|
+
'CRITICAL': '🔴',
|
|
249
|
+
'HIGH': '🟠',
|
|
250
|
+
'MEDIUM': '🟡',
|
|
251
|
+
'LOW': '🟢',
|
|
252
|
+
'MINIMAL': '⚪'
|
|
253
|
+
}.get(score.priority_level, '⚪')
|
|
254
|
+
|
|
255
|
+
print(f"### {i}. {emoji} {score.filepath}")
|
|
256
|
+
print()
|
|
257
|
+
print(f"**Score:** {score.priority_score:.0f} ({score.priority_level})")
|
|
258
|
+
print()
|
|
259
|
+
print(f"- Complexity: {score.complexity_score}")
|
|
260
|
+
print(f"- Security: {score.security_score}")
|
|
261
|
+
print(f"- Dependencies: {score.dependency_score}")
|
|
262
|
+
print(f"- Annotation Coverage: {score.annotation_coverage * 100:.0f}%")
|
|
263
|
+
print()
|
|
264
|
+
|
|
265
|
+
if score.reasons:
|
|
266
|
+
print("**Reasons:**")
|
|
267
|
+
print()
|
|
268
|
+
for reason in score.reasons:
|
|
269
|
+
print(f"- {reason}")
|
|
270
|
+
print()
|
|
271
|
+
|
|
272
|
+
if score.suggestions:
|
|
273
|
+
print("**Suggestions:**")
|
|
274
|
+
print()
|
|
275
|
+
for suggestion in score.suggestions:
|
|
276
|
+
print(f"- {suggestion}")
|
|
277
|
+
print()
|
|
File without changes
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main Priority Analyzer for VooDocs
|
|
3
|
+
|
|
4
|
+
Combines complexity, security, and dependency analysis to prioritize files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from .complexity import ComplexityAnalyzer
|
|
14
|
+
from .security import SecurityAnalyzer
|
|
15
|
+
from .dependencies import DependencyAnalyzer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class FileScore:
|
|
20
|
+
"""Score and analysis for a single file."""
|
|
21
|
+
filepath: str
|
|
22
|
+
priority_score: float
|
|
23
|
+
complexity_score: int
|
|
24
|
+
security_score: int
|
|
25
|
+
dependency_score: int
|
|
26
|
+
annotation_penalty: int
|
|
27
|
+
priority_level: str
|
|
28
|
+
reasons: List[str]
|
|
29
|
+
suggestions: List[str]
|
|
30
|
+
|
|
31
|
+
# Detailed metrics
|
|
32
|
+
loc: int = 0
|
|
33
|
+
cyclomatic: int = 0
|
|
34
|
+
functions: int = 0
|
|
35
|
+
security_keywords: List[str] = None
|
|
36
|
+
import_count: int = 0
|
|
37
|
+
dependent_count: int = 0
|
|
38
|
+
annotation_coverage: float = 0.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PriorityAnalyzer:
|
|
42
|
+
"""Main analyzer that combines all scoring components."""
|
|
43
|
+
|
|
44
|
+
# Scoring weights
|
|
45
|
+
COMPLEXITY_WEIGHT = 0.30
|
|
46
|
+
SECURITY_WEIGHT = 0.40
|
|
47
|
+
DEPENDENCY_WEIGHT = 0.20
|
|
48
|
+
ANNOTATION_WEIGHT = 0.10
|
|
49
|
+
|
|
50
|
+
# Priority level thresholds
|
|
51
|
+
PRIORITY_THRESHOLDS = {
|
|
52
|
+
'CRITICAL': 80,
|
|
53
|
+
'HIGH': 60,
|
|
54
|
+
'MEDIUM': 40,
|
|
55
|
+
'LOW': 20,
|
|
56
|
+
'MINIMAL': 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Supported file extensions
|
|
60
|
+
SUPPORTED_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.sol'}
|
|
61
|
+
|
|
62
|
+
def __init__(self, project_root: str = None):
|
|
63
|
+
"""
|
|
64
|
+
Initialize priority analyzer.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
project_root: Root directory of the project
|
|
68
|
+
"""
|
|
69
|
+
self.project_root = project_root
|
|
70
|
+
self.complexity_analyzer = ComplexityAnalyzer()
|
|
71
|
+
self.security_analyzer = SecurityAnalyzer()
|
|
72
|
+
self.dependency_analyzer = DependencyAnalyzer(project_root)
|
|
73
|
+
|
|
74
|
+
def analyze_file(self, filepath: str, all_files: List[str] = None) -> FileScore:
|
|
75
|
+
"""
|
|
76
|
+
Analyze a single file and calculate priority score.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
filepath: Path to file to analyze
|
|
80
|
+
all_files: List of all files (for dependency analysis)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
FileScore object with complete analysis
|
|
84
|
+
"""
|
|
85
|
+
# Run individual analyzers
|
|
86
|
+
complexity = self.complexity_analyzer.analyze_file(filepath)
|
|
87
|
+
security = self.security_analyzer.analyze_file(filepath)
|
|
88
|
+
dependencies = self.dependency_analyzer.analyze_file(filepath, all_files)
|
|
89
|
+
|
|
90
|
+
# Check annotation coverage
|
|
91
|
+
annotation_coverage, annotation_penalty = self._check_annotation_coverage(filepath)
|
|
92
|
+
|
|
93
|
+
# Calculate weighted priority score
|
|
94
|
+
priority_score = (
|
|
95
|
+
complexity['total_score'] * self.COMPLEXITY_WEIGHT +
|
|
96
|
+
security['total_score'] * self.SECURITY_WEIGHT +
|
|
97
|
+
dependencies['total_score'] * self.DEPENDENCY_WEIGHT +
|
|
98
|
+
annotation_penalty * self.ANNOTATION_WEIGHT
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Determine priority level
|
|
102
|
+
priority_level = self._get_priority_level(priority_score)
|
|
103
|
+
|
|
104
|
+
# Generate reasons and suggestions
|
|
105
|
+
reasons = self._generate_reasons(complexity, security, dependencies, annotation_coverage)
|
|
106
|
+
suggestions = self._generate_suggestions(complexity, security, dependencies, annotation_coverage)
|
|
107
|
+
|
|
108
|
+
# Combine all security keywords
|
|
109
|
+
all_keywords = (
|
|
110
|
+
security.get('critical_keywords', []) +
|
|
111
|
+
security.get('high_keywords', []) +
|
|
112
|
+
security.get('medium_keywords', [])
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return FileScore(
|
|
116
|
+
filepath=filepath,
|
|
117
|
+
priority_score=round(priority_score, 1),
|
|
118
|
+
complexity_score=complexity['total_score'],
|
|
119
|
+
security_score=security['total_score'],
|
|
120
|
+
dependency_score=dependencies['total_score'],
|
|
121
|
+
annotation_penalty=annotation_penalty,
|
|
122
|
+
priority_level=priority_level,
|
|
123
|
+
reasons=reasons,
|
|
124
|
+
suggestions=suggestions,
|
|
125
|
+
loc=complexity.get('loc', 0),
|
|
126
|
+
cyclomatic=complexity.get('cyclomatic', 0),
|
|
127
|
+
functions=complexity.get('functions', 0),
|
|
128
|
+
security_keywords=all_keywords[:10], # Top 10
|
|
129
|
+
import_count=dependencies.get('import_count', 0),
|
|
130
|
+
dependent_count=dependencies.get('dependent_count', 0),
|
|
131
|
+
annotation_coverage=annotation_coverage
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def analyze_directory(self, dirpath: str, recursive: bool = True,
|
|
135
|
+
exclude_patterns: List[str] = None) -> List[FileScore]:
|
|
136
|
+
"""
|
|
137
|
+
Analyze all files in a directory.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
dirpath: Directory path to analyze
|
|
141
|
+
recursive: Whether to scan subdirectories
|
|
142
|
+
exclude_patterns: Patterns to exclude (e.g., ['node_modules', 'dist'])
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of FileScore objects, sorted by priority (highest first)
|
|
146
|
+
"""
|
|
147
|
+
if exclude_patterns is None:
|
|
148
|
+
exclude_patterns = ['node_modules', 'dist', 'build', '.git', '__pycache__']
|
|
149
|
+
|
|
150
|
+
# Find all files
|
|
151
|
+
all_files = self._find_files(dirpath, recursive, exclude_patterns)
|
|
152
|
+
|
|
153
|
+
# Analyze each file
|
|
154
|
+
scores = []
|
|
155
|
+
for filepath in all_files:
|
|
156
|
+
try:
|
|
157
|
+
score = self.analyze_file(filepath, all_files)
|
|
158
|
+
scores.append(score)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f"Warning: Failed to analyze {filepath}: {e}")
|
|
161
|
+
|
|
162
|
+
# Sort by priority score (highest first)
|
|
163
|
+
scores.sort(key=lambda x: x.priority_score, reverse=True)
|
|
164
|
+
|
|
165
|
+
return scores
|
|
166
|
+
|
|
167
|
+
def _find_files(self, dirpath: str, recursive: bool, exclude_patterns: List[str]) -> List[str]:
|
|
168
|
+
"""Find all supported files in directory."""
|
|
169
|
+
files = []
|
|
170
|
+
|
|
171
|
+
if recursive:
|
|
172
|
+
for root, dirs, filenames in os.walk(dirpath):
|
|
173
|
+
# Filter out excluded directories
|
|
174
|
+
dirs[:] = [d for d in dirs if not any(pattern in d for pattern in exclude_patterns)]
|
|
175
|
+
|
|
176
|
+
for filename in filenames:
|
|
177
|
+
filepath = os.path.join(root, filename)
|
|
178
|
+
if Path(filepath).suffix in self.SUPPORTED_EXTENSIONS:
|
|
179
|
+
files.append(filepath)
|
|
180
|
+
else:
|
|
181
|
+
for filename in os.listdir(dirpath):
|
|
182
|
+
filepath = os.path.join(dirpath, filename)
|
|
183
|
+
if os.path.isfile(filepath) and Path(filepath).suffix in self.SUPPORTED_EXTENSIONS:
|
|
184
|
+
files.append(filepath)
|
|
185
|
+
|
|
186
|
+
return files
|
|
187
|
+
|
|
188
|
+
def _check_annotation_coverage(self, filepath: str) -> tuple:
|
|
189
|
+
"""
|
|
190
|
+
Check VooDocs annotation coverage in file.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Tuple of (coverage_percentage, penalty_score)
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
197
|
+
content = f.read()
|
|
198
|
+
except:
|
|
199
|
+
return (0.0, -50)
|
|
200
|
+
|
|
201
|
+
# Count @darkarts annotations
|
|
202
|
+
darkarts_count = len(re.findall(r'@darkarts(-lite)?', content))
|
|
203
|
+
|
|
204
|
+
# Count total functions (rough estimate)
|
|
205
|
+
function_patterns = [
|
|
206
|
+
r'^\s*def\s+\w+\s*\(', # Python
|
|
207
|
+
r'^\s*(export\s+)?(async\s+)?function\s+\w+\s*\(', # JS/TS function
|
|
208
|
+
r'^\s*(const|let|var)\s+\w+\s*=\s*(\([^)]*\)|[^=]+)\s*=>', # Arrow function
|
|
209
|
+
r'^\s*function\s+\w+\s*\(', # Solidity
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
total_functions = 0
|
|
213
|
+
for pattern in function_patterns:
|
|
214
|
+
total_functions += len(re.findall(pattern, content, re.MULTILINE))
|
|
215
|
+
|
|
216
|
+
# Calculate coverage
|
|
217
|
+
if total_functions == 0:
|
|
218
|
+
# No functions, check if file has any annotations
|
|
219
|
+
coverage = 1.0 if darkarts_count > 0 else 0.0
|
|
220
|
+
else:
|
|
221
|
+
coverage = min(1.0, darkarts_count / total_functions)
|
|
222
|
+
|
|
223
|
+
# Calculate penalty
|
|
224
|
+
if coverage == 0:
|
|
225
|
+
penalty = -50
|
|
226
|
+
elif coverage < 0.5:
|
|
227
|
+
penalty = -25
|
|
228
|
+
else:
|
|
229
|
+
penalty = 0
|
|
230
|
+
|
|
231
|
+
return (coverage, penalty)
|
|
232
|
+
|
|
233
|
+
def _get_priority_level(self, score: float) -> str:
|
|
234
|
+
"""Determine priority level from score."""
|
|
235
|
+
if score >= self.PRIORITY_THRESHOLDS['CRITICAL']:
|
|
236
|
+
return 'CRITICAL'
|
|
237
|
+
elif score >= self.PRIORITY_THRESHOLDS['HIGH']:
|
|
238
|
+
return 'HIGH'
|
|
239
|
+
elif score >= self.PRIORITY_THRESHOLDS['MEDIUM']:
|
|
240
|
+
return 'MEDIUM'
|
|
241
|
+
elif score >= self.PRIORITY_THRESHOLDS['LOW']:
|
|
242
|
+
return 'LOW'
|
|
243
|
+
else:
|
|
244
|
+
return 'MINIMAL'
|
|
245
|
+
|
|
246
|
+
def _generate_reasons(self, complexity: Dict, security: Dict,
|
|
247
|
+
dependencies: Dict, annotation_coverage: float) -> List[str]:
|
|
248
|
+
"""Generate human-readable reasons for priority score."""
|
|
249
|
+
reasons = []
|
|
250
|
+
|
|
251
|
+
# Complexity reasons
|
|
252
|
+
if complexity['total_score'] >= 60:
|
|
253
|
+
reasons.append(f"High complexity: {complexity['loc']} LOC, {complexity['functions']} functions")
|
|
254
|
+
elif complexity['total_score'] >= 30:
|
|
255
|
+
reasons.append(f"Medium complexity: {complexity['loc']} LOC, {complexity['functions']} functions")
|
|
256
|
+
|
|
257
|
+
# Security reasons
|
|
258
|
+
if security.get('critical_keywords'):
|
|
259
|
+
keywords = ', '.join(security['critical_keywords'][:5])
|
|
260
|
+
reasons.append(f"Security keywords: {keywords}")
|
|
261
|
+
|
|
262
|
+
# Dependency reasons
|
|
263
|
+
if dependencies.get('dependent_count', 0) > 0:
|
|
264
|
+
reasons.append(f"Depended upon by {dependencies['dependent_count']} file(s)")
|
|
265
|
+
|
|
266
|
+
# Annotation reasons
|
|
267
|
+
if annotation_coverage == 0:
|
|
268
|
+
reasons.append("No VooDocs annotations found")
|
|
269
|
+
elif annotation_coverage < 0.5:
|
|
270
|
+
reasons.append(f"Partial annotations ({int(annotation_coverage * 100)}% coverage)")
|
|
271
|
+
|
|
272
|
+
return reasons
|
|
273
|
+
|
|
274
|
+
def _generate_suggestions(self, complexity: Dict, security: Dict,
|
|
275
|
+
dependencies: Dict, annotation_coverage: float) -> List[str]:
|
|
276
|
+
"""Generate actionable suggestions."""
|
|
277
|
+
suggestions = []
|
|
278
|
+
|
|
279
|
+
# Security suggestions
|
|
280
|
+
suggestions.extend(self.security_analyzer.get_security_suggestions(security))
|
|
281
|
+
|
|
282
|
+
# Dependency suggestions
|
|
283
|
+
suggestions.extend(self.dependency_analyzer.get_dependency_suggestions(dependencies))
|
|
284
|
+
|
|
285
|
+
# Complexity suggestions
|
|
286
|
+
if complexity['total_score'] >= 60:
|
|
287
|
+
suggestions.append("Break down complex logic into smaller functions")
|
|
288
|
+
suggestions.append("Document high-complexity sections")
|
|
289
|
+
|
|
290
|
+
# Annotation suggestions
|
|
291
|
+
if annotation_coverage == 0:
|
|
292
|
+
suggestions.append("Add @darkarts annotations to all functions")
|
|
293
|
+
suggestions.append("Start with module-level purpose annotation")
|
|
294
|
+
elif annotation_coverage < 1.0:
|
|
295
|
+
suggestions.append("Complete remaining function annotations")
|
|
296
|
+
|
|
297
|
+
# Generic suggestions
|
|
298
|
+
if not suggestions:
|
|
299
|
+
suggestions.append("Add comprehensive VooDocs annotations")
|
|
300
|
+
|
|
301
|
+
return suggestions[:5] # Limit to top 5
|