qgis-plugin-analyzer 1.3.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.
- __init__.py +19 -0
- analyzer/__init__.py +19 -0
- analyzer/cli.py +311 -0
- analyzer/engine.py +586 -0
- analyzer/fixer.py +314 -0
- analyzer/models/__init__.py +5 -0
- analyzer/models/analysis_models.py +62 -0
- analyzer/reporters/__init__.py +10 -0
- analyzer/reporters/html_reporter.py +388 -0
- analyzer/reporters/markdown_reporter.py +212 -0
- analyzer/reporters/summary_reporter.py +222 -0
- analyzer/rules/__init__.py +10 -0
- analyzer/rules/modernization_rules.py +33 -0
- analyzer/rules/qgis_rules.py +74 -0
- analyzer/scanner.py +794 -0
- analyzer/semantic.py +213 -0
- analyzer/transformers.py +190 -0
- analyzer/utils/__init__.py +39 -0
- analyzer/utils/ast_utils.py +133 -0
- analyzer/utils/config_utils.py +145 -0
- analyzer/utils/logging_utils.py +46 -0
- analyzer/utils/path_utils.py +135 -0
- analyzer/utils/performance_utils.py +150 -0
- analyzer/validators.py +263 -0
- qgis_plugin_analyzer-1.3.0.dist-info/METADATA +239 -0
- qgis_plugin_analyzer-1.3.0.dist-info/RECORD +30 -0
- qgis_plugin_analyzer-1.3.0.dist-info/WHEEL +5 -0
- qgis_plugin_analyzer-1.3.0.dist-info/entry_points.txt +2 -0
- qgis_plugin_analyzer-1.3.0.dist-info/licenses/LICENSE +677 -0
- qgis_plugin_analyzer-1.3.0.dist-info/top_level.txt +2 -0
__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# /***************************************************************************
|
|
2
|
+
# QGIS Plugin Analyzer
|
|
3
|
+
# A QGIS tool
|
|
4
|
+
# Static code analysis and standards audit for QGIS plugins.
|
|
5
|
+
# -------------------
|
|
6
|
+
# begin : 2025-12-28
|
|
7
|
+
# git sha : $Format:%H$
|
|
8
|
+
# copyright : (C) 2025 by Juan M Bernales
|
|
9
|
+
# email : juanbernales@gmail.com
|
|
10
|
+
# ***************************************************************************/
|
|
11
|
+
#
|
|
12
|
+
# /***************************************************************************
|
|
13
|
+
# * *
|
|
14
|
+
# * This program is free software; you can redistribute it and/or modify *
|
|
15
|
+
# * it under the terms of the GNU General Public License as published by *
|
|
16
|
+
# * the Free Software Foundation; either version 2 of the License, or *
|
|
17
|
+
# * (at your option) any later version. *
|
|
18
|
+
# * *
|
|
19
|
+
# ***************************************************************************/
|
analyzer/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# /***************************************************************************
|
|
2
|
+
# QGIS Plugin Analyzer
|
|
3
|
+
# A QGIS tool
|
|
4
|
+
# Static code analysis and standards audit for QGIS plugins.
|
|
5
|
+
# -------------------
|
|
6
|
+
# begin : 2025-12-28
|
|
7
|
+
# git sha : $Format:%H$
|
|
8
|
+
# copyright : (C) 2025 by Juan M Bernales
|
|
9
|
+
# email : juanbernales@gmail.com
|
|
10
|
+
# ***************************************************************************/
|
|
11
|
+
#
|
|
12
|
+
# /***************************************************************************
|
|
13
|
+
# * *
|
|
14
|
+
# * This program is free software; you can redistribute it and/or modify *
|
|
15
|
+
# * it under the terms of the GNU General Public License as published by *
|
|
16
|
+
# * the Free Software Foundation; either version 2 of the License, or *
|
|
17
|
+
# * (at your option) any later version. *
|
|
18
|
+
# * *
|
|
19
|
+
# ***************************************************************************/
|
analyzer/cli.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# /***************************************************************************
|
|
2
|
+
# QGIS Plugin Analyzer
|
|
3
|
+
# A QGIS tool
|
|
4
|
+
# Static code analysis and standards audit for QGIS plugins.
|
|
5
|
+
# -------------------
|
|
6
|
+
# begin : 2025-12-28
|
|
7
|
+
# git sha : $Format:%H$
|
|
8
|
+
# copyright : (C) 2025 by Juan M Bernales
|
|
9
|
+
# email : juanbernales@gmail.com
|
|
10
|
+
# ***************************************************************************/
|
|
11
|
+
#
|
|
12
|
+
# /***************************************************************************
|
|
13
|
+
# * *
|
|
14
|
+
# * This program is free software; you can redistribute it and/or modify *
|
|
15
|
+
# * it under the terms of the GNU General Public License as published by *
|
|
16
|
+
# * the Free Software Foundation; either version 2 of the License, or *
|
|
17
|
+
# * (at your option) any later version. *
|
|
18
|
+
# * *
|
|
19
|
+
# ***************************************************************************/
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import pathlib
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
from .engine import ProjectAnalyzer
|
|
27
|
+
from .utils import logger, setup_logger
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _setup_argument_parser() -> argparse.ArgumentParser:
|
|
31
|
+
"""Sets up and returns the argument parser with all subcommands.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A configured ArgumentParser instance.
|
|
35
|
+
"""
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
description="QGIS Plugin Analyzer - A guardian for your PyQGIS code"
|
|
38
|
+
)
|
|
39
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
|
40
|
+
|
|
41
|
+
# Analyze Command
|
|
42
|
+
analyze_parser = subparsers.add_parser("analyze", help="Analyze an existing QGIS plugin")
|
|
43
|
+
analyze_parser.add_argument("project_path", help="Path to the QGIS project to analyze")
|
|
44
|
+
analyze_parser.add_argument(
|
|
45
|
+
"-o",
|
|
46
|
+
"--output",
|
|
47
|
+
help="Output directory for reports",
|
|
48
|
+
default="./analysis_results",
|
|
49
|
+
)
|
|
50
|
+
analyze_parser.add_argument(
|
|
51
|
+
"-r",
|
|
52
|
+
"--report",
|
|
53
|
+
action="store_true",
|
|
54
|
+
help="Generate detailed HTML/Markdown reports",
|
|
55
|
+
)
|
|
56
|
+
analyze_parser.add_argument(
|
|
57
|
+
"-p",
|
|
58
|
+
"--profile",
|
|
59
|
+
help="Configuration profile from pyproject.toml",
|
|
60
|
+
default="default",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Fix Command
|
|
64
|
+
fix_parser = subparsers.add_parser("fix", help="Auto-fix common QGIS plugin issues")
|
|
65
|
+
fix_parser.add_argument("path", type=str, help="Path to the QGIS plugin directory")
|
|
66
|
+
fix_parser.add_argument(
|
|
67
|
+
"--dry-run",
|
|
68
|
+
action="store_true",
|
|
69
|
+
default=True,
|
|
70
|
+
help="Show proposed changes without applying (default: True)",
|
|
71
|
+
)
|
|
72
|
+
fix_parser.add_argument("--apply", action="store_true", help="Apply fixes (disables dry-run)")
|
|
73
|
+
fix_parser.add_argument(
|
|
74
|
+
"--auto-approve",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Apply all fixes without confirmation",
|
|
77
|
+
)
|
|
78
|
+
fix_parser.add_argument(
|
|
79
|
+
"-p",
|
|
80
|
+
"--profile",
|
|
81
|
+
help="Configuration profile from pyproject.toml",
|
|
82
|
+
default="default",
|
|
83
|
+
)
|
|
84
|
+
fix_parser.add_argument(
|
|
85
|
+
"--rules",
|
|
86
|
+
type=str,
|
|
87
|
+
help="Comma-separated list of rule IDs to fix",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# List Rules Command
|
|
91
|
+
subparsers.add_parser("list-rules", help="List all available QGIS audit rules")
|
|
92
|
+
|
|
93
|
+
# Init Command
|
|
94
|
+
subparsers.add_parser("init", help="Initialize a new .analyzerignore with defaults")
|
|
95
|
+
|
|
96
|
+
# Summary Command
|
|
97
|
+
summary_parser = subparsers.add_parser(
|
|
98
|
+
"summary", help="Show a quick terminal summary of analysis results"
|
|
99
|
+
)
|
|
100
|
+
summary_parser.add_argument(
|
|
101
|
+
"-i",
|
|
102
|
+
"--input",
|
|
103
|
+
help="Path to the research JSON file",
|
|
104
|
+
default="analysis_results/project_context.json",
|
|
105
|
+
)
|
|
106
|
+
summary_parser.add_argument(
|
|
107
|
+
"-b",
|
|
108
|
+
"--by",
|
|
109
|
+
choices=["total", "modules", "functions", "classes"],
|
|
110
|
+
default="total",
|
|
111
|
+
help="Granularity of the summary (default: total)",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return parser
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _handle_fix_command(args: argparse.Namespace) -> bool:
|
|
118
|
+
"""Handles the execution of the 'fix' command.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
args: Parsed command line arguments.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if the fix process completed successfully, False otherwise.
|
|
125
|
+
"""
|
|
126
|
+
import json
|
|
127
|
+
|
|
128
|
+
from .fixer import AutoFixer
|
|
129
|
+
|
|
130
|
+
project_path = pathlib.Path(args.path).resolve()
|
|
131
|
+
if not project_path.exists():
|
|
132
|
+
print(f"❌ Path not found: {project_path}")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# Run analysis first
|
|
136
|
+
print("🔍 Analyzing project for fixable issues...")
|
|
137
|
+
analyzer = ProjectAnalyzer(
|
|
138
|
+
str(project_path),
|
|
139
|
+
args.output if hasattr(args, "output") else "./analysis_results",
|
|
140
|
+
args.profile if hasattr(args, "profile") else "default",
|
|
141
|
+
)
|
|
142
|
+
analyzer.run()
|
|
143
|
+
|
|
144
|
+
# Load issues
|
|
145
|
+
context_file = analyzer.output_dir / "project_context.json"
|
|
146
|
+
with open(context_file) as f:
|
|
147
|
+
context = json.load(f)
|
|
148
|
+
|
|
149
|
+
all_issues = []
|
|
150
|
+
for module in context.get("modules", []):
|
|
151
|
+
all_issues.extend(module.get("ast_issues", []))
|
|
152
|
+
|
|
153
|
+
if args.rules:
|
|
154
|
+
rule_ids = [r.strip() for r in args.rules.split(",")]
|
|
155
|
+
all_issues = [i for i in all_issues if i.get("type") in rule_ids]
|
|
156
|
+
|
|
157
|
+
fixer = AutoFixer(project_path, dry_run=not args.apply)
|
|
158
|
+
fixable = fixer.get_fixable_issues(all_issues)
|
|
159
|
+
|
|
160
|
+
if not fixable:
|
|
161
|
+
print("✅ No fixable issues found!")
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
print(f"\n📋 Found {len(fixable)} fixable issue(s)")
|
|
165
|
+
if not args.apply:
|
|
166
|
+
print("\n⚠️ DRY RUN MODE (use --apply to execute changes)\n")
|
|
167
|
+
|
|
168
|
+
stats = fixer.apply_fixes(fixable, interactive=not args.auto_approve)
|
|
169
|
+
print(
|
|
170
|
+
f"\n📊 Summary: Applied: {stats['applied']}, Skipped: {stats['skipped']}, Failed: {stats['failed']}"
|
|
171
|
+
)
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _handle_analyze_command(args: argparse.Namespace) -> None:
|
|
176
|
+
"""Handles the execution of the 'analyze' command.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
args: Parsed command line arguments.
|
|
180
|
+
"""
|
|
181
|
+
# Force generate_html based on flag, overriding profile if necessary for CLI usage
|
|
182
|
+
# We pass it via a temporary config override or modify the analyzer init
|
|
183
|
+
# For now, let's pass it to the analyzer constructor or modify config after init
|
|
184
|
+
|
|
185
|
+
analyzer = ProjectAnalyzer(args.project_path, args.output, args.profile)
|
|
186
|
+
|
|
187
|
+
# Override config based on CLI flag
|
|
188
|
+
if hasattr(args, "report") and args.report:
|
|
189
|
+
analyzer.config["generate_html"] = True
|
|
190
|
+
else:
|
|
191
|
+
analyzer.config["generate_html"] = False
|
|
192
|
+
|
|
193
|
+
success = analyzer.run()
|
|
194
|
+
|
|
195
|
+
# Always show terminal summary
|
|
196
|
+
from .reporters.summary_reporter import report_summary
|
|
197
|
+
|
|
198
|
+
# If we didn't generate reports, we might still want to show the summary
|
|
199
|
+
# using the in-memory data or the context file if it was saved.
|
|
200
|
+
# Engine saves json context by default? Let's check engine.py.
|
|
201
|
+
# Assuming engine saves project_context.json always or we need to access results directly.
|
|
202
|
+
# To keep it simple, we depend on the engine saving the context or returning it.
|
|
203
|
+
# Current engine.run retuns bool.
|
|
204
|
+
|
|
205
|
+
context_path = analyzer.output_dir / "project_context.json"
|
|
206
|
+
if context_path.exists():
|
|
207
|
+
report_summary(context_path)
|
|
208
|
+
|
|
209
|
+
if not success:
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _handle_list_rules_command() -> None:
|
|
214
|
+
"""Handles the 'list-rules' command by displaying available audit rules."""
|
|
215
|
+
from .rules import get_qgis_audit_rules
|
|
216
|
+
|
|
217
|
+
rules = get_qgis_audit_rules()
|
|
218
|
+
print("\n📋 QGIS Audit Rules Catalog:")
|
|
219
|
+
print("=" * 30)
|
|
220
|
+
for r in rules:
|
|
221
|
+
print(f"- [{r['severity'].upper()}] {r['id']}: {r['message']}")
|
|
222
|
+
print(f"\nTotal: {len(rules)} rules.\n")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _handle_init_command() -> None:
|
|
226
|
+
"""Handles the 'init' command by creating a default .analyzerignore file."""
|
|
227
|
+
from .utils import DEFAULT_EXCLUDE
|
|
228
|
+
|
|
229
|
+
ignore_file = pathlib.Path(".analyzerignore")
|
|
230
|
+
if ignore_file.exists():
|
|
231
|
+
print("⚠️ .analyzerignore already exists. Skipping.")
|
|
232
|
+
else:
|
|
233
|
+
with open(ignore_file, "w") as f:
|
|
234
|
+
f.write("# QGIS Plugin Analyzer Ignore File\n")
|
|
235
|
+
for p in DEFAULT_EXCLUDE:
|
|
236
|
+
f.write(f"{p}\n")
|
|
237
|
+
print("✅ Created .analyzerignore with default excludes.")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _handle_summary_command(args: argparse.Namespace) -> None:
|
|
241
|
+
"""Handles the 'summary' command by displaying a terminal report.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
args: Parsed command line arguments.
|
|
245
|
+
"""
|
|
246
|
+
from .reporters.summary_reporter import report_summary
|
|
247
|
+
|
|
248
|
+
input_path = pathlib.Path(args.input).resolve()
|
|
249
|
+
report_summary(input_path, by=args.by)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def main() -> None:
|
|
253
|
+
"""Main entry point for the QGIS Plugin Analyzer CLI.
|
|
254
|
+
|
|
255
|
+
Orchestrates the command execution based on parsed arguments and
|
|
256
|
+
sets up the global logging environment.
|
|
257
|
+
"""
|
|
258
|
+
parser = _setup_argument_parser()
|
|
259
|
+
|
|
260
|
+
# Legacy support / default to analyze if no command provided
|
|
261
|
+
if len(sys.argv) > 1 and sys.argv[1] not in [
|
|
262
|
+
"analyze",
|
|
263
|
+
"fix",
|
|
264
|
+
"list-rules",
|
|
265
|
+
"init",
|
|
266
|
+
"summary",
|
|
267
|
+
"-h",
|
|
268
|
+
"--help",
|
|
269
|
+
]:
|
|
270
|
+
# If the first argument is a path (doesn't start with -), assume 'analyze'
|
|
271
|
+
if not sys.argv[1].startswith("-"):
|
|
272
|
+
sys.argv.insert(1, "analyze")
|
|
273
|
+
|
|
274
|
+
args = parser.parse_args()
|
|
275
|
+
|
|
276
|
+
# Initialize logger (default to analysis_results if not specified)
|
|
277
|
+
output_dir = pathlib.Path(getattr(args, "output", "./analysis_results")).resolve()
|
|
278
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
setup_logger(output_dir)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
if args.command == "fix":
|
|
283
|
+
_handle_fix_command(args)
|
|
284
|
+
elif args.command == "analyze":
|
|
285
|
+
_handle_analyze_command(args)
|
|
286
|
+
elif args.command == "list-rules":
|
|
287
|
+
_handle_list_rules_command()
|
|
288
|
+
elif args.command == "init":
|
|
289
|
+
_handle_init_command()
|
|
290
|
+
elif args.command == "summary":
|
|
291
|
+
_handle_summary_command(args)
|
|
292
|
+
else:
|
|
293
|
+
parser.print_help()
|
|
294
|
+
|
|
295
|
+
except KeyboardInterrupt:
|
|
296
|
+
logger.info("\n⏹️ Analysis interrupted.")
|
|
297
|
+
sys.exit(1)
|
|
298
|
+
except FileNotFoundError as e:
|
|
299
|
+
logger.error(f"Error: File not found: {e}")
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
except ValueError as e:
|
|
302
|
+
# This handles path traversal or other validation errors
|
|
303
|
+
logger.error(f"Error: {e}")
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.critical(f"Critical Error: {e}", exc_info=True)
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
main()
|