qgis-plugin-analyzer 1.4.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 (41) hide show
  1. analyzer/__init__.py +2 -1
  2. analyzer/cli/__init__.py +14 -0
  3. analyzer/cli/app.py +147 -0
  4. analyzer/cli/base.py +93 -0
  5. analyzer/cli/commands/__init__.py +19 -0
  6. analyzer/cli/commands/analyze.py +47 -0
  7. analyzer/cli/commands/fix.py +58 -0
  8. analyzer/cli/commands/init.py +41 -0
  9. analyzer/cli/commands/list_rules.py +41 -0
  10. analyzer/cli/commands/security.py +46 -0
  11. analyzer/cli/commands/summary.py +52 -0
  12. analyzer/cli/commands/version.py +41 -0
  13. analyzer/cli.py +4 -281
  14. analyzer/commands.py +163 -0
  15. analyzer/engine.py +491 -245
  16. analyzer/fixer.py +206 -130
  17. analyzer/reporters/markdown_reporter.py +88 -14
  18. analyzer/reporters/summary_reporter.py +226 -49
  19. analyzer/rules/qgis_rules.py +3 -1
  20. analyzer/scanner.py +219 -711
  21. analyzer/secrets.py +84 -0
  22. analyzer/security_checker.py +85 -0
  23. analyzer/security_rules.py +127 -0
  24. analyzer/transformers.py +29 -8
  25. analyzer/utils/__init__.py +2 -0
  26. analyzer/utils/path_utils.py +53 -1
  27. analyzer/validators.py +90 -55
  28. analyzer/visitors/__init__.py +19 -0
  29. analyzer/visitors/base.py +75 -0
  30. analyzer/visitors/composite_visitor.py +73 -0
  31. analyzer/visitors/imports_visitor.py +85 -0
  32. analyzer/visitors/metrics_visitor.py +158 -0
  33. analyzer/visitors/security_visitor.py +52 -0
  34. analyzer/visitors/standards_visitor.py +284 -0
  35. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/METADATA +32 -10
  36. qgis_plugin_analyzer-1.6.0.dist-info/RECORD +52 -0
  37. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/WHEEL +1 -1
  38. qgis_plugin_analyzer-1.4.0.dist-info/RECORD +0 -30
  39. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/entry_points.txt +0 -0
  40. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/licenses/LICENSE +0 -0
  41. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/top_level.txt +0 -0
analyzer/cli.py CHANGED
@@ -19,292 +19,15 @@
19
19
  # ***************************************************************************/
20
20
 
21
21
 
22
- import argparse
23
- import pathlib
24
22
  import sys
25
23
 
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)
24
+ from .cli import CLIApp
250
25
 
251
26
 
252
27
  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)
28
+ """Main entry point for the QGIS Plugin Analyzer CLI."""
29
+ app = CLIApp()
30
+ sys.exit(app.run())
308
31
 
309
32
 
310
33
  if __name__ == "__main__":
analyzer/commands.py ADDED
@@ -0,0 +1,163 @@
1
+ """Command handlers for the QGIS Plugin Analyzer CLI.
2
+
3
+ This module contains the implementation of individual CLI commands to separate
4
+ interface definition (cli.py) from execution logic.
5
+ """
6
+
7
+ import argparse
8
+ import dataclasses
9
+ import json
10
+ import pathlib
11
+ import sys
12
+
13
+ from .engine import ProjectAnalyzer
14
+ from .fixer import AutoFixer
15
+ from .reporters.summary_reporter import report_summary
16
+ from .rules import get_qgis_audit_rules
17
+ from .utils import DEFAULT_EXCLUDE
18
+
19
+
20
+ def handle_fix(args: argparse.Namespace) -> bool:
21
+ """Handles the execution of the 'fix' command.
22
+
23
+ Args:
24
+ args: Parsed command line arguments.
25
+
26
+ Returns:
27
+ True if the fix process completed successfully, False otherwise.
28
+ """
29
+ project_path = pathlib.Path(args.path).resolve()
30
+ if not project_path.exists():
31
+ print(f"❌ Path not found: {project_path}")
32
+ return False
33
+
34
+ # Run analysis first
35
+ print("🔍 Analyzing project for fixable issues...")
36
+ analyzer = ProjectAnalyzer(
37
+ str(project_path),
38
+ args.output if hasattr(args, "output") else "./analysis_results",
39
+ args.profile if hasattr(args, "profile") else "default",
40
+ )
41
+ analyzer.run()
42
+
43
+ # Load issues
44
+ context_file = analyzer.output_dir / "project_context.json"
45
+ if not context_file.exists():
46
+ print("❌ Analysis failed to generate context file.")
47
+ return False
48
+
49
+ with open(context_file) as f:
50
+ context = json.load(f)
51
+
52
+ all_issues = []
53
+ for module in context.get("modules", []):
54
+ all_issues.extend(module.get("ast_issues", []))
55
+
56
+ if args.rules:
57
+ rule_ids = [r.strip() for r in args.rules.split(",")]
58
+ all_issues = [i for i in all_issues if i.get("type") in rule_ids]
59
+
60
+ fixer = AutoFixer(project_path, dry_run=not args.apply)
61
+ fixable = fixer.get_fixable_issues(all_issues)
62
+
63
+ if not fixable:
64
+ print("✅ No fixable issues found!")
65
+ return True
66
+
67
+ print(f"\n📋 Found {len(fixable)} fixable issue(s)")
68
+ if not args.apply:
69
+ print("\n⚠️ DRY RUN MODE (use --apply to execute changes)\n")
70
+
71
+ stats = fixer.apply_fixes(fixable, interactive=not args.auto_approve)
72
+ print(
73
+ f"\n📊 Summary: Applied: {stats['applied']}, Skipped: {stats['skipped']}, Failed: {stats['failed']}"
74
+ )
75
+ return True
76
+
77
+
78
+ def handle_analyze(args: argparse.Namespace) -> None:
79
+ """Handles the execution of the 'analyze' command.
80
+
81
+ Args:
82
+ args: Parsed command line arguments.
83
+ """
84
+ analyzer = ProjectAnalyzer(args.project_path, args.output, args.profile)
85
+
86
+ # Override config based on CLI flag using dataclasses.replace since it's frozen
87
+ report_enabled = bool(hasattr(args, "report") and args.report)
88
+ analyzer.config = dataclasses.replace(analyzer.config, generate_html=report_enabled)
89
+
90
+ success = analyzer.run()
91
+
92
+ context_path = analyzer.output_dir / "project_context.json"
93
+ if context_path.exists():
94
+ report_summary(context_path)
95
+
96
+ if not success:
97
+ sys.exit(1)
98
+
99
+
100
+ def handle_list_rules() -> None:
101
+ """Handles the 'list-rules' command by displaying available audit rules."""
102
+ rules = get_qgis_audit_rules()
103
+ print("\n📋 QGIS Audit Rules Catalog:")
104
+ print("=" * 30)
105
+ for r in rules:
106
+ print(f"- [{r['severity'].upper()}] {r['id']}: {r['message']}")
107
+ print(f"\nTotal: {len(rules)} rules.\n")
108
+
109
+
110
+ def handle_init() -> None:
111
+ """Handles the 'init' command by creating a default .analyzerignore file."""
112
+ ignore_file = pathlib.Path(".analyzerignore")
113
+ if ignore_file.exists():
114
+ print("⚠️ .analyzerignore already exists. Skipping.")
115
+ else:
116
+ with open(ignore_file, "w") as f:
117
+ f.write("# QGIS Plugin Analyzer Ignore File\n")
118
+ for p in DEFAULT_EXCLUDE:
119
+ f.write(f"{p}\n")
120
+ print("✅ Created .analyzerignore with default excludes.")
121
+
122
+
123
+ def handle_summary(args: argparse.Namespace) -> None:
124
+ """Handles the 'summary' command by displaying a terminal report.
125
+
126
+ Args:
127
+ args: Parsed command line arguments.
128
+ """
129
+ input_path = pathlib.Path(args.input).resolve()
130
+ report_summary(input_path, by=args.by)
131
+
132
+
133
+ def handle_security(args: argparse.Namespace) -> None:
134
+ """Handles the execution of the 'security' command.
135
+
136
+ Args:
137
+ args: Parsed command line arguments.
138
+ """
139
+ project_path = pathlib.Path(args.project_path).resolve()
140
+ if not project_path.exists():
141
+ print(f"❌ Path not found: {project_path}")
142
+ sys.exit(1)
143
+
144
+ print(f"🛡️ Starting focused security scan for: {project_path.name}...")
145
+
146
+ # Run analyzer with current profile
147
+ analyzer = ProjectAnalyzer(str(project_path), args.output, args.profile)
148
+
149
+ # We could potentially add high-level flags here
150
+ if args.deep:
151
+ # Note: ProjectConfig currently doesn't have security_deep_scan,
152
+ # but if it did, we would use dataclasses.replace here too.
153
+ print("🔍 Deep scan enabled (Entropy analysis and full secret detection)")
154
+
155
+ success = analyzer.run()
156
+
157
+ context_path = analyzer.output_dir / "project_context.json"
158
+ if context_path.exists():
159
+ # Use the specialized security reporter
160
+ report_summary(context_path, by="security")
161
+
162
+ if not success:
163
+ sys.exit(1)