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.
Files changed (37) hide show
  1. analyzer/cli/__init__.py +14 -0
  2. analyzer/cli/app.py +147 -0
  3. analyzer/cli/base.py +93 -0
  4. analyzer/cli/commands/__init__.py +19 -0
  5. analyzer/cli/commands/analyze.py +47 -0
  6. analyzer/cli/commands/fix.py +58 -0
  7. analyzer/cli/commands/init.py +41 -0
  8. analyzer/cli/commands/list_rules.py +41 -0
  9. analyzer/cli/commands/security.py +46 -0
  10. analyzer/cli/commands/summary.py +52 -0
  11. analyzer/cli/commands/version.py +41 -0
  12. analyzer/cli.py +4 -184
  13. analyzer/commands.py +7 -7
  14. analyzer/engine.py +421 -238
  15. analyzer/fixer.py +206 -130
  16. analyzer/reporters/markdown_reporter.py +48 -15
  17. analyzer/reporters/summary_reporter.py +193 -80
  18. analyzer/scanner.py +218 -138
  19. analyzer/transformers.py +29 -8
  20. analyzer/utils/__init__.py +2 -0
  21. analyzer/utils/path_utils.py +53 -1
  22. analyzer/validators.py +90 -55
  23. analyzer/visitors/__init__.py +19 -0
  24. analyzer/visitors/base.py +75 -0
  25. analyzer/visitors/composite_visitor.py +73 -0
  26. analyzer/visitors/imports_visitor.py +85 -0
  27. analyzer/visitors/metrics_visitor.py +158 -0
  28. analyzer/visitors/security_visitor.py +52 -0
  29. analyzer/visitors/standards_visitor.py +284 -0
  30. {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/METADATA +16 -7
  31. qgis_plugin_analyzer-1.6.0.dist-info/RECORD +52 -0
  32. analyzer/visitors.py +0 -455
  33. qgis_plugin_analyzer-1.5.0.dist-info/RECORD +0 -35
  34. {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/WHEEL +0 -0
  35. {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/entry_points.txt +0 -0
  36. {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/licenses/LICENSE +0 -0
  37. {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
- def _report_total(data: Dict[str, Any]) -> bool:
75
- """Prints the executive total summary."""
76
- print("\n\033[1m📋 QGIS Plugin Analyzer: Project Summary\033[0m")
77
- print("=" * 45)
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
- # 1. Quality Indicators
80
- metrics = data.get("metrics", {})
81
- print("\n\033[1m📊 Quality Indicators\033[0m")
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
- # 2. Research Metrics
87
- research = data.get("research_summary", {})
88
- if research:
89
- print("\n\033[1m🔬 Research-based Metrics\033[0m")
90
- params_cov = research.get("type_hint_coverage", 0)
91
- returns_cov = research.get("return_hint_coverage", 0)
92
- doc_cov = research.get("docstring_coverage", 0)
93
- styles = research.get("detected_docstring_styles", [])
94
- style = styles[0] if styles else "Unknown"
95
-
96
- print(f"- Type Hint Coverage (Params): {params_cov:.1f}%")
97
- print(f"- Type Hint Coverage (Returns): {returns_cov:.1f}%")
98
- print(f"- Docstring Coverage: {doc_cov:.1f}%")
99
- print(f"- Documentation Style: {style}")
100
-
101
- # 3. Issue Summary
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 Security Findings
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
- print("\n\033[92m✅ No issues detected! Your project looks great.\033[0m")
213
+ print_success("✅ No issues detected! Your project looks great.")
117
214
  else:
118
- print(f"\n\033[1m⚠️ Issue Statistics ({len(issues)} total)\033[0m")
119
- counts: Dict[str, int] = {}
120
- for i in issues:
121
- t = i.get("type", "unknown")
122
- counts[t] = counts.get(t, 0) + 1
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
- print("\n\033[1m🛡️ QGIS Plugin Analyzer: Security Scan\033[0m")
243
- print("=" * 60)
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
- print("\n\033[92m✅ No security vulnerabilities found!\033[0m")
393
+ print_success("✅ No security vulnerabilities found!")
254
394
  else:
255
- print("\n\033[1m🛑 Detailed Findings\033[0m")
256
- print("-" * 60)
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
- print("=" * 60)
398
+ print_separator("=", 60)
286
399
  return True