code-compass-cli 0.1.0__py3-none-any.whl
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.
- code_compass_cli-0.1.0.dist-info/METADATA +46 -0
- code_compass_cli-0.1.0.dist-info/RECORD +31 -0
- code_compass_cli-0.1.0.dist-info/WHEEL +5 -0
- code_compass_cli-0.1.0.dist-info/entry_points.txt +2 -0
- code_compass_cli-0.1.0.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/__pycache__/__init__.cpython-313.pyc +0 -0
- src/cli/__init__.py +1 -0
- src/cli/__pycache__/__init__.cpython-313.pyc +0 -0
- src/cli/__pycache__/main.cpython-313.pyc +0 -0
- src/cli/main.py +431 -0
- src/docs/__init__.py +1 -0
- src/docs/__pycache__/__init__.cpython-313.pyc +0 -0
- src/docs/__pycache__/doc_generator.cpython-313.pyc +0 -0
- src/docs/doc_generator.py +734 -0
- src/quality/__init__.py +1 -0
- src/quality/__pycache__/__init__.cpython-313.pyc +0 -0
- src/quality/__pycache__/analyzer.cpython-313.pyc +0 -0
- src/quality/analyzer.py +300 -0
- src/query/__init__.py +1 -0
- src/query/__pycache__/__init__.cpython-313.pyc +0 -0
- src/query/__pycache__/copilot_query.cpython-313.pyc +0 -0
- src/query/copilot_query.py +475 -0
- src/scanner/__init__.py +1 -0
- src/scanner/__pycache__/__init__.cpython-313.pyc +0 -0
- src/scanner/__pycache__/repo_scanner.cpython-313.pyc +0 -0
- src/scanner/repo_scanner.py +139 -0
- src/visualizer/__init__.py +1 -0
- src/visualizer/__pycache__/__init__.cpython-313.pyc +0 -0
- src/visualizer/__pycache__/flow_tracer.cpython-313.pyc +0 -0
- src/visualizer/flow_tracer.py +294 -0
src/quality/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Code quality analysis module."""
|
|
Binary file
|
|
Binary file
|
src/quality/analyzer.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Code quality analyzer for detecting security, performance, and code smell issues."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Any, Optional
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Issue:
|
|
12
|
+
"""Represents a detected code issue."""
|
|
13
|
+
|
|
14
|
+
file: str
|
|
15
|
+
line: int
|
|
16
|
+
severity: str # "critical", "warning", "info"
|
|
17
|
+
category: str # "security", "performance", "code_smell"
|
|
18
|
+
message: str
|
|
19
|
+
suggestion: str
|
|
20
|
+
code_snippet: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CodeAnalyzer:
|
|
24
|
+
"""Analyzes code for security issues, performance problems, and code smells."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, repo_path: str = "."):
|
|
27
|
+
"""Initialize the code analyzer.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
repo_path: Path to repository to analyze
|
|
31
|
+
"""
|
|
32
|
+
self.repo_path = Path(repo_path)
|
|
33
|
+
self.issues: List[Issue] = []
|
|
34
|
+
|
|
35
|
+
def analyze(self) -> Dict[str, Any]:
|
|
36
|
+
"""Analyze the codebase for issues.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dictionary with analysis results organized by severity
|
|
40
|
+
"""
|
|
41
|
+
self.issues = []
|
|
42
|
+
|
|
43
|
+
# Scan all Python files
|
|
44
|
+
for root, _, files in os.walk(self.repo_path):
|
|
45
|
+
# Skip venv and hidden directories
|
|
46
|
+
if "venv" in root or "/.git" in root:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
for file in files:
|
|
50
|
+
if file.endswith(".py"):
|
|
51
|
+
filepath = Path(root) / file
|
|
52
|
+
rel_path = filepath.relative_to(self.repo_path)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
|
56
|
+
content = f.read()
|
|
57
|
+
self._analyze_file(content, str(rel_path))
|
|
58
|
+
except (IOError, OSError):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Organize by severity
|
|
62
|
+
critical = [i for i in self.issues if i.severity == "critical"]
|
|
63
|
+
warnings = [i for i in self.issues if i.severity == "warning"]
|
|
64
|
+
info = [i for i in self.issues if i.severity == "info"]
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"total": len(self.issues),
|
|
68
|
+
"critical": critical,
|
|
69
|
+
"warning": warnings,
|
|
70
|
+
"info": info,
|
|
71
|
+
"by_category": self._group_by_category(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def _analyze_file(self, content: str, filepath: str) -> None:
|
|
75
|
+
"""Analyze a single file for issues.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
content: File content
|
|
79
|
+
filepath: Path to the file
|
|
80
|
+
"""
|
|
81
|
+
self._check_security(content, filepath)
|
|
82
|
+
self._check_performance(content, filepath)
|
|
83
|
+
self._check_code_smells(content, filepath)
|
|
84
|
+
|
|
85
|
+
def _check_security(self, content: str, filepath: str) -> None:
|
|
86
|
+
"""Check for security issues.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
content: File content
|
|
90
|
+
filepath: Path to the file
|
|
91
|
+
"""
|
|
92
|
+
lines = content.split("\n")
|
|
93
|
+
|
|
94
|
+
# Check for SQL injection patterns
|
|
95
|
+
sql_patterns = [
|
|
96
|
+
(r'(?:query|execute|sql)\s*\(\s*["\'].*%s', "Potential SQL injection - use parameterized queries"),
|
|
97
|
+
(r'(?:query|execute|sql)\s*\(\s*f["\'].*{', "Potential SQL injection - f-strings are vulnerable"),
|
|
98
|
+
(r'\.format\s*\(.*\)\s*for\s+(?:query|execute)', "SQL string formatting detected"),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for i, line in enumerate(lines, 1):
|
|
102
|
+
for pattern, msg in sql_patterns:
|
|
103
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
104
|
+
self.issues.append(Issue(
|
|
105
|
+
file=filepath,
|
|
106
|
+
line=i,
|
|
107
|
+
severity="critical",
|
|
108
|
+
category="security",
|
|
109
|
+
message=f"SQL Injection Risk: {msg}",
|
|
110
|
+
suggestion="Use parameterized queries/prepared statements",
|
|
111
|
+
code_snippet=line.strip()[:80],
|
|
112
|
+
))
|
|
113
|
+
|
|
114
|
+
# Check for exposed secrets/API keys
|
|
115
|
+
secret_patterns = [
|
|
116
|
+
(r'(?:api[_-]?key|secret|password|token|auth)\s*=\s*["\'][^"\']*["\']', "Hardcoded secret/API key"),
|
|
117
|
+
(r'(?:sk_|pk_|Bearer\s+)[A-Za-z0-9\-_]+', "Exposed API key or token"),
|
|
118
|
+
(r'https?://\w+:\w+@', "Credentials in URL"),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for i, line in enumerate(lines, 1):
|
|
122
|
+
# Skip if line is commented
|
|
123
|
+
if line.strip().startswith("#"):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
for pattern, msg in secret_patterns:
|
|
127
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
128
|
+
self.issues.append(Issue(
|
|
129
|
+
file=filepath,
|
|
130
|
+
line=i,
|
|
131
|
+
severity="critical",
|
|
132
|
+
category="security",
|
|
133
|
+
message=f"Exposed Secret: {msg}",
|
|
134
|
+
suggestion="Move secrets to environment variables or configuration files",
|
|
135
|
+
code_snippet=line.strip()[:80],
|
|
136
|
+
))
|
|
137
|
+
|
|
138
|
+
# Check for XSS vulnerabilities
|
|
139
|
+
xss_patterns = [
|
|
140
|
+
(r'\.innerHTML\s*=\s*(?!.*escape)', "Direct HTML assignment - XSS risk"),
|
|
141
|
+
(r'(?:html|render)\s*\(.*\+.*(?:user_input|request\.args|request\.form)', "User input in HTML - XSS risk"),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
for i, line in enumerate(lines, 1):
|
|
145
|
+
for pattern, msg in xss_patterns:
|
|
146
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
147
|
+
self.issues.append(Issue(
|
|
148
|
+
file=filepath,
|
|
149
|
+
line=i,
|
|
150
|
+
severity="warning",
|
|
151
|
+
category="security",
|
|
152
|
+
message=f"XSS Vulnerability: {msg}",
|
|
153
|
+
suggestion="Escape/sanitize all user input before rendering",
|
|
154
|
+
code_snippet=line.strip()[:80],
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
def _check_performance(self, content: str, filepath: str) -> None:
|
|
158
|
+
"""Check for performance issues.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
content: File content
|
|
162
|
+
filepath: Path to the file
|
|
163
|
+
"""
|
|
164
|
+
lines = content.split("\n")
|
|
165
|
+
|
|
166
|
+
# Check for N+1 query patterns
|
|
167
|
+
for i in range(len(lines) - 1):
|
|
168
|
+
line = lines[i]
|
|
169
|
+
|
|
170
|
+
# Look for loops with potential queries inside
|
|
171
|
+
if re.match(r'^\s*for\s+\w+\s+in\s+', line):
|
|
172
|
+
# Check next few lines for query patterns
|
|
173
|
+
for j in range(i + 1, min(i + 5, len(lines))):
|
|
174
|
+
if re.search(r'(?:query|execute|filter|get|fetch)\s*\(', lines[j]):
|
|
175
|
+
self.issues.append(Issue(
|
|
176
|
+
file=filepath,
|
|
177
|
+
line=i + 1,
|
|
178
|
+
severity="warning",
|
|
179
|
+
category="performance",
|
|
180
|
+
message="Potential N+1 Query Problem",
|
|
181
|
+
suggestion="Consider using batch queries or joins instead of looping",
|
|
182
|
+
code_snippet=line.strip()[:80],
|
|
183
|
+
))
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
# Check for inefficient loop patterns
|
|
187
|
+
for i, line in enumerate(lines, 1):
|
|
188
|
+
# Nested loops
|
|
189
|
+
if re.match(r'^\s{8,}for\s+', line): # Deeply indented for loop
|
|
190
|
+
for j in range(max(0, i - 5), i):
|
|
191
|
+
if re.match(r'^\s{4,8}for\s+', lines[j - 1]):
|
|
192
|
+
self.issues.append(Issue(
|
|
193
|
+
file=filepath,
|
|
194
|
+
line=i,
|
|
195
|
+
severity="info",
|
|
196
|
+
category="performance",
|
|
197
|
+
message="Nested Loop Detected",
|
|
198
|
+
suggestion="Consider using list comprehension or optimization",
|
|
199
|
+
code_snippet=line.strip()[:80],
|
|
200
|
+
))
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
def _check_code_smells(self, content: str, filepath: str) -> None:
|
|
204
|
+
"""Check for code smells.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
content: File content
|
|
208
|
+
filepath: Path to the file
|
|
209
|
+
"""
|
|
210
|
+
lines = content.split("\n")
|
|
211
|
+
|
|
212
|
+
# Check for long functions (more than 30 lines)
|
|
213
|
+
current_func = None
|
|
214
|
+
func_start = 0
|
|
215
|
+
|
|
216
|
+
for i, line in enumerate(lines, 1):
|
|
217
|
+
# Find function definitions
|
|
218
|
+
func_match = re.match(r"^\s*def\s+(\w+)\s*\(", line)
|
|
219
|
+
if func_match:
|
|
220
|
+
if current_func and i - func_start > 30:
|
|
221
|
+
self.issues.append(Issue(
|
|
222
|
+
file=filepath,
|
|
223
|
+
line=func_start,
|
|
224
|
+
severity="info",
|
|
225
|
+
category="code_smell",
|
|
226
|
+
message=f"Function '{current_func}' is too long ({i - func_start} lines)",
|
|
227
|
+
suggestion="Consider breaking this function into smaller, more focused functions",
|
|
228
|
+
code_snippet=f"def {current_func}(...)",
|
|
229
|
+
))
|
|
230
|
+
|
|
231
|
+
current_func = func_match.group(1)
|
|
232
|
+
func_start = i
|
|
233
|
+
|
|
234
|
+
# Check final function
|
|
235
|
+
if current_func and len(lines) - func_start > 30:
|
|
236
|
+
self.issues.append(Issue(
|
|
237
|
+
file=filepath,
|
|
238
|
+
line=func_start,
|
|
239
|
+
severity="info",
|
|
240
|
+
category="code_smell",
|
|
241
|
+
message=f"Function '{current_func}' is too long ({len(lines) - func_start} lines)",
|
|
242
|
+
suggestion="Consider breaking this function into smaller, more focused functions",
|
|
243
|
+
code_snippet=f"def {current_func}(...)",
|
|
244
|
+
))
|
|
245
|
+
|
|
246
|
+
# Check for duplicate code patterns (simple heuristic)
|
|
247
|
+
code_lines = [line.strip() for line in lines if line.strip() and not line.strip().startswith("#")]
|
|
248
|
+
seen = {}
|
|
249
|
+
|
|
250
|
+
for i, line in enumerate(code_lines):
|
|
251
|
+
# Only check substantial lines
|
|
252
|
+
if len(line) > 20:
|
|
253
|
+
if line in seen:
|
|
254
|
+
# Found duplicate code
|
|
255
|
+
if i - seen[line] > 5: # Not too close
|
|
256
|
+
self.issues.append(Issue(
|
|
257
|
+
file=filepath,
|
|
258
|
+
line=i + 1,
|
|
259
|
+
severity="info",
|
|
260
|
+
category="code_smell",
|
|
261
|
+
message="Duplicate Code Detected",
|
|
262
|
+
suggestion="Extract this logic into a reusable function",
|
|
263
|
+
code_snippet=line[:80],
|
|
264
|
+
))
|
|
265
|
+
else:
|
|
266
|
+
seen[line] = i
|
|
267
|
+
|
|
268
|
+
# Check for large classes (more than 50 lines)
|
|
269
|
+
current_class = None
|
|
270
|
+
class_start = 0
|
|
271
|
+
|
|
272
|
+
for i, line in enumerate(lines, 1):
|
|
273
|
+
class_match = re.match(r"^\s*class\s+(\w+)\s*[\(:]", line)
|
|
274
|
+
if class_match:
|
|
275
|
+
if current_class and i - class_start > 50:
|
|
276
|
+
self.issues.append(Issue(
|
|
277
|
+
file=filepath,
|
|
278
|
+
line=class_start,
|
|
279
|
+
severity="info",
|
|
280
|
+
category="code_smell",
|
|
281
|
+
message=f"Class '{current_class}' is too large ({i - class_start} lines)",
|
|
282
|
+
suggestion="Consider breaking this class using composition or inheritance",
|
|
283
|
+
code_snippet=f"class {current_class}:",
|
|
284
|
+
))
|
|
285
|
+
|
|
286
|
+
current_class = class_match.group(1)
|
|
287
|
+
class_start = i
|
|
288
|
+
|
|
289
|
+
def _group_by_category(self) -> Dict[str, List[Issue]]:
|
|
290
|
+
"""Group issues by category.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dictionary of issues grouped by category
|
|
294
|
+
"""
|
|
295
|
+
grouped = {}
|
|
296
|
+
for issue in self.issues:
|
|
297
|
+
if issue.category not in grouped:
|
|
298
|
+
grouped[issue.category] = []
|
|
299
|
+
grouped[issue.category].append(issue)
|
|
300
|
+
return grouped
|
src/query/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Query module for code analysis."""
|
|
Binary file
|
|
Binary file
|