qgis-plugin-analyzer 1.5.0__py3-none-any.whl → 1.6.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.
- analyzer/cli/__init__.py +14 -0
- analyzer/cli/app.py +147 -0
- analyzer/cli/base.py +93 -0
- analyzer/cli/commands/__init__.py +19 -0
- analyzer/cli/commands/analyze.py +47 -0
- analyzer/cli/commands/fix.py +58 -0
- analyzer/cli/commands/init.py +41 -0
- analyzer/cli/commands/list_rules.py +41 -0
- analyzer/cli/commands/security.py +46 -0
- analyzer/cli/commands/summary.py +52 -0
- analyzer/cli/commands/version.py +41 -0
- analyzer/cli.py +4 -184
- analyzer/commands.py +7 -7
- analyzer/engine.py +421 -238
- analyzer/fixer.py +206 -130
- analyzer/reporters/markdown_reporter.py +48 -15
- analyzer/reporters/summary_reporter.py +193 -80
- analyzer/scanner.py +218 -138
- analyzer/transformers.py +29 -8
- analyzer/utils/__init__.py +2 -0
- analyzer/utils/path_utils.py +53 -1
- analyzer/validators.py +90 -55
- analyzer/visitors/__init__.py +19 -0
- analyzer/visitors/base.py +75 -0
- analyzer/visitors/composite_visitor.py +73 -0
- analyzer/visitors/imports_visitor.py +85 -0
- analyzer/visitors/metrics_visitor.py +158 -0
- analyzer/visitors/security_visitor.py +52 -0
- analyzer/visitors/standards_visitor.py +284 -0
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/METADATA +16 -7
- qgis_plugin_analyzer-1.6.0.dist-info/RECORD +52 -0
- analyzer/visitors.py +0 -455
- qgis_plugin_analyzer-1.5.0.dist-info/RECORD +0 -35
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/WHEEL +0 -0
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/entry_points.txt +0 -0
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,36 @@ import json
|
|
|
9
9
|
import pathlib
|
|
10
10
|
from typing import Any, Dict, List
|
|
11
11
|
|
|
12
|
+
# Helper functions for formatting
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print_header(title: str) -> None:
|
|
16
|
+
"""Print a formatted section header.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
title: The header title to display.
|
|
20
|
+
"""
|
|
21
|
+
print(f"\n\033[1m{title}\033[0m")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def print_separator(char: str = "=", length: int = 45) -> None:
|
|
25
|
+
"""Print a separator line.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
char: The character to use for the separator.
|
|
29
|
+
length: The length of the separator line.
|
|
30
|
+
"""
|
|
31
|
+
print(char * length)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def print_success(message: str) -> None:
|
|
35
|
+
"""Print a success message in green.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
message: The success message to display.
|
|
39
|
+
"""
|
|
40
|
+
print(f"\n\033[92m{message}\033[0m")
|
|
41
|
+
|
|
12
42
|
|
|
13
43
|
def print_colored_score(label: str, score: Any) -> None:
|
|
14
44
|
"""Prints a score with ANSI colors based on its value.
|
|
@@ -71,72 +101,122 @@ def report_summary(input_path: pathlib.Path, by: str = "total") -> bool:
|
|
|
71
101
|
return False
|
|
72
102
|
|
|
73
103
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
# Specialized methods for _report_total
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _print_quality_indicators(metrics: Dict[str, Any]) -> None:
|
|
108
|
+
"""Print quality scores section.
|
|
78
109
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
Args:
|
|
111
|
+
metrics: Dictionary containing quality metrics.
|
|
112
|
+
"""
|
|
113
|
+
print_header("📊 Quality Indicators")
|
|
82
114
|
print_colored_score("- Module Stability Score", metrics.get("quality_score", "N/A"))
|
|
83
115
|
print_colored_score("- Code Maintainability Score", metrics.get("maintainability_score", "N/A"))
|
|
84
116
|
print_colored_score("- Security Score (Bandit)", metrics.get("security_score", "N/A"))
|
|
85
117
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
|
|
119
|
+
def _print_research_metrics(research: Dict[str, Any]) -> None:
|
|
120
|
+
"""Print research-based metrics section.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
research: Dictionary containing research metrics.
|
|
124
|
+
"""
|
|
125
|
+
if not research:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
print_header("🔬 Research-based Metrics")
|
|
129
|
+
params_cov = research.get("type_hint_coverage", 0)
|
|
130
|
+
returns_cov = research.get("return_hint_coverage", 0)
|
|
131
|
+
doc_cov = research.get("docstring_coverage", 0)
|
|
132
|
+
styles = research.get("detected_docstring_styles", [])
|
|
133
|
+
style = styles[0] if styles else "Unknown"
|
|
134
|
+
|
|
135
|
+
print(f"- Type Hint Coverage (Params): {params_cov:.1f}%")
|
|
136
|
+
print(f"- Type Hint Coverage (Returns): {returns_cov:.1f}%")
|
|
137
|
+
print(f"- Docstring Coverage: {doc_cov:.1f}%")
|
|
138
|
+
print(f"- Documentation Style: {style}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _collect_all_issues(data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
142
|
+
"""Collect and merge AST issues and security findings.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
data: The full analysis results dictionary.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of all issues with file paths added.
|
|
149
|
+
"""
|
|
102
150
|
issues: List[Dict[str, Any]] = []
|
|
151
|
+
|
|
152
|
+
# Collect AST issues
|
|
103
153
|
for module in data.get("modules", []):
|
|
104
154
|
mod_path = module.get("path", "unknown")
|
|
105
155
|
for issue in module.get("ast_issues", []):
|
|
106
156
|
issue["file"] = mod_path
|
|
107
157
|
issues.append(issue)
|
|
108
158
|
|
|
109
|
-
# Add
|
|
159
|
+
# Add security findings
|
|
110
160
|
security_findings = data.get("security", {}).get("findings", [])
|
|
111
161
|
for finding in security_findings:
|
|
112
162
|
finding["type"] = f"SECURITY:{finding.get('type', 'generic')}"
|
|
113
163
|
issues.append(finding)
|
|
114
164
|
|
|
165
|
+
return issues
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _print_issue_statistics(issues: List[Dict[str, Any]]) -> None:
|
|
169
|
+
"""Print issue counts grouped by type.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
issues: List of all issues.
|
|
173
|
+
"""
|
|
174
|
+
print(f"\n\033[1m⚠️ Issue Statistics ({len(issues)} total)\033[0m")
|
|
175
|
+
counts: Dict[str, int] = {}
|
|
176
|
+
for issue in issues:
|
|
177
|
+
issue_type = issue.get("type", "unknown")
|
|
178
|
+
counts[issue_type] = counts.get(issue_type, 0) + 1
|
|
179
|
+
|
|
180
|
+
for issue_type, count in sorted(counts.items(), key=lambda x: x[1], reverse=True):
|
|
181
|
+
print(f"- {issue_type}: {count}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _print_sample_issues(issues: List[Dict[str, Any]], limit: int = 5) -> None:
|
|
185
|
+
"""Print sample issues with formatting.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
issues: List of all issues.
|
|
189
|
+
limit: Maximum number of issues to display.
|
|
190
|
+
"""
|
|
191
|
+
print_header("🔍 Sample Issues")
|
|
192
|
+
for issue in issues[:limit]:
|
|
193
|
+
severity = issue.get("severity", "info").upper()
|
|
194
|
+
sev_color = "\033[91m" if severity == "ERROR" else "\033[93m"
|
|
195
|
+
print(
|
|
196
|
+
f"{sev_color}[{severity}]\033[0m {issue['file']}:{issue.get('line', '?')} - {issue['message']}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if len(issues) > limit:
|
|
200
|
+
print(f"... and {len(issues) - limit} more issues.")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _report_total(data: Dict[str, Any]) -> bool:
|
|
204
|
+
"""Prints the executive total summary."""
|
|
205
|
+
print_header("📋 QGIS Plugin Analyzer: Project Summary")
|
|
206
|
+
print_separator()
|
|
207
|
+
|
|
208
|
+
_print_quality_indicators(data.get("metrics", {}))
|
|
209
|
+
_print_research_metrics(data.get("research_summary", {}))
|
|
210
|
+
|
|
211
|
+
issues = _collect_all_issues(data)
|
|
115
212
|
if not issues:
|
|
116
|
-
|
|
213
|
+
print_success("✅ No issues detected! Your project looks great.")
|
|
117
214
|
else:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
for t, c in sorted(counts.items(), key=lambda x: x[1], reverse=True):
|
|
125
|
-
print(f"- {t}: {c}")
|
|
126
|
-
|
|
127
|
-
# 4. Sample Issues
|
|
128
|
-
print("\n\033[1m🔍 Sample Issues\033[0m")
|
|
129
|
-
for i in issues[:5]:
|
|
130
|
-
severity = i.get("severity", "info").upper()
|
|
131
|
-
sev_color = "\033[91m" if severity == "ERROR" else "\033[93m"
|
|
132
|
-
print(
|
|
133
|
-
f"{sev_color}[{severity}]\033[0m {i['file']}:{i.get('line', '?')} - {i['message']}"
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if len(issues) > 5:
|
|
137
|
-
print(f"... and {len(issues) - 5} more issues.")
|
|
138
|
-
|
|
139
|
-
print("\n" + "=" * 45)
|
|
215
|
+
_print_issue_statistics(issues)
|
|
216
|
+
_print_sample_issues(issues)
|
|
217
|
+
|
|
218
|
+
print()
|
|
219
|
+
print_separator()
|
|
140
220
|
return True
|
|
141
221
|
|
|
142
222
|
|
|
@@ -230,6 +310,66 @@ def _report_by_classes(data: Dict[str, Any]) -> bool:
|
|
|
230
310
|
return True
|
|
231
311
|
|
|
232
312
|
|
|
313
|
+
# Specialized methods for _report_security
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _group_findings_by_severity(findings: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
317
|
+
"""Group security findings by severity level.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
findings: List of security findings.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Dictionary mapping severity levels to lists of findings.
|
|
324
|
+
"""
|
|
325
|
+
by_severity: Dict[str, List[Dict[str, Any]]] = {"high": [], "medium": [], "low": []}
|
|
326
|
+
for finding in findings:
|
|
327
|
+
sev = finding.get("severity", "medium").lower()
|
|
328
|
+
if sev in by_severity:
|
|
329
|
+
by_severity[sev].append(finding)
|
|
330
|
+
else:
|
|
331
|
+
by_severity.setdefault("other", []).append(finding)
|
|
332
|
+
return by_severity
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _print_security_finding(finding: Dict[str, Any], severity: str) -> None:
|
|
336
|
+
"""Print a single security finding with formatting.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
finding: The security finding dictionary.
|
|
340
|
+
severity: The severity level (high, medium, low).
|
|
341
|
+
"""
|
|
342
|
+
sev_color = (
|
|
343
|
+
"\033[91m" if severity == "high" else ("\033[93m" if severity == "medium" else "\033[94m")
|
|
344
|
+
)
|
|
345
|
+
print(
|
|
346
|
+
f"{sev_color}[{severity.upper()}]\033[0m {finding.get('file')}:{finding.get('line')} - {finding.get('type')}"
|
|
347
|
+
)
|
|
348
|
+
print(f" \033[2mMessage: {finding.get('message')}\033[0m")
|
|
349
|
+
code_snippet = finding.get("code")
|
|
350
|
+
if isinstance(code_snippet, str) and code_snippet.strip():
|
|
351
|
+
print(f" \033[2mCode : {code_snippet.strip()}\033[0m")
|
|
352
|
+
print()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _print_security_findings_by_severity(by_severity: Dict[str, List[Dict[str, Any]]]) -> None:
|
|
356
|
+
"""Print all findings grouped by severity.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
by_severity: Dictionary mapping severity levels to findings.
|
|
360
|
+
"""
|
|
361
|
+
print_header("🛑 Detailed Findings")
|
|
362
|
+
print_separator("-", 60)
|
|
363
|
+
|
|
364
|
+
for severity in ["high", "medium", "low"]:
|
|
365
|
+
group = by_severity.get(severity, [])
|
|
366
|
+
if not group:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
for finding in group:
|
|
370
|
+
_print_security_finding(finding, severity)
|
|
371
|
+
|
|
372
|
+
|
|
233
373
|
def _report_security(data: Dict[str, Any]) -> bool:
|
|
234
374
|
"""Prints a focused security analysis report.
|
|
235
375
|
|
|
@@ -239,8 +379,8 @@ def _report_security(data: Dict[str, Any]) -> bool:
|
|
|
239
379
|
Returns:
|
|
240
380
|
True if the report was successfully generated.
|
|
241
381
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
382
|
+
print_header("🛡️ QGIS Plugin Analyzer: Security Scan")
|
|
383
|
+
print_separator("=", 60)
|
|
244
384
|
|
|
245
385
|
security = data.get("security", {})
|
|
246
386
|
findings = security.get("findings", [])
|
|
@@ -250,37 +390,10 @@ def _report_security(data: Dict[str, Any]) -> bool:
|
|
|
250
390
|
print(f"Total vulnerabilities detected: {len(findings)}")
|
|
251
391
|
|
|
252
392
|
if not findings:
|
|
253
|
-
|
|
393
|
+
print_success("✅ No security vulnerabilities found!")
|
|
254
394
|
else:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
# Group by severity
|
|
259
|
-
by_severity: Dict[str, List[Dict[str, Any]]] = {"high": [], "medium": [], "low": []}
|
|
260
|
-
for f in findings:
|
|
261
|
-
sev = f.get("severity", "medium").lower()
|
|
262
|
-
if sev in by_severity:
|
|
263
|
-
by_severity[sev].append(f)
|
|
264
|
-
else:
|
|
265
|
-
by_severity.setdefault("other", []).append(f)
|
|
266
|
-
|
|
267
|
-
for sev in ["high", "medium", "low"]:
|
|
268
|
-
group = by_severity.get(sev, [])
|
|
269
|
-
if not group:
|
|
270
|
-
continue
|
|
271
|
-
|
|
272
|
-
sev_color = (
|
|
273
|
-
"\033[91m" if sev == "high" else ("\033[93m" if sev == "medium" else "\033[94m")
|
|
274
|
-
)
|
|
275
|
-
for f in group:
|
|
276
|
-
print(
|
|
277
|
-
f"{sev_color}[{sev.upper()}]\033[0m {f.get('file')}:{f.get('line')} - {f.get('type')}"
|
|
278
|
-
)
|
|
279
|
-
print(f" \033[2mMessage: {f.get('message')}\033[0m")
|
|
280
|
-
code_snippet = f.get("code")
|
|
281
|
-
if isinstance(code_snippet, str) and code_snippet.strip():
|
|
282
|
-
print(f" \033[2mCode : {code_snippet.strip()}\033[0m")
|
|
283
|
-
print()
|
|
395
|
+
by_severity = _group_findings_by_severity(findings)
|
|
396
|
+
_print_security_findings_by_severity(by_severity)
|
|
284
397
|
|
|
285
|
-
|
|
398
|
+
print_separator("=", 60)
|
|
286
399
|
return True
|