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/__init__.py CHANGED
@@ -16,4 +16,5 @@
16
16
  # * the Free Software Foundation; either version 2 of the License, or *
17
17
  # * (at your option) any later version. *
18
18
  # * *
19
- # ***************************************************************************/
19
+ #
20
+ __version__ = "1.5.0"
@@ -0,0 +1,14 @@
1
+ """CLI package for QGIS Plugin Analyzer."""
2
+
3
+ import sys
4
+
5
+ from .app import CLIApp
6
+
7
+
8
+ def main() -> None:
9
+ """Main entry point for the QGIS Plugin Analyzer CLI."""
10
+ app = CLIApp()
11
+ sys.exit(app.run())
12
+
13
+
14
+ __all__ = ["CLIApp", "main"]
analyzer/cli/app.py ADDED
@@ -0,0 +1,147 @@
1
+ """CLI Application orchestrator."""
2
+
3
+ import argparse
4
+ import pathlib
5
+ import sys
6
+ from typing import Dict, List, Optional
7
+
8
+ from ..utils import logger, setup_logger
9
+ from .base import BaseCommand
10
+ from .commands import (
11
+ AnalyzeCommand,
12
+ FixCommand,
13
+ InitCommand,
14
+ ListRulesCommand,
15
+ SecurityCommand,
16
+ SummaryCommand,
17
+ VersionCommand,
18
+ )
19
+
20
+
21
+ class CLIApp:
22
+ """Main CLI application orchestrator.
23
+
24
+ Manages command registration, argument parsing, and execution.
25
+ """
26
+
27
+ def __init__(self):
28
+ """Initialize the CLI application with all available commands."""
29
+ self.commands: Dict[str, BaseCommand] = self._discover_commands()
30
+
31
+ def _discover_commands(self) -> Dict[str, BaseCommand]:
32
+ """Auto-discover and instantiate all command classes.
33
+
34
+ Returns:
35
+ Dictionary mapping command names to command instances.
36
+ """
37
+ command_classes: List[type[BaseCommand]] = [
38
+ AnalyzeCommand,
39
+ SecurityCommand,
40
+ FixCommand,
41
+ ListRulesCommand,
42
+ InitCommand,
43
+ SummaryCommand,
44
+ VersionCommand,
45
+ ]
46
+ return {cmd().name: cmd() for cmd in command_classes}
47
+
48
+ def _build_parser(self) -> argparse.ArgumentParser:
49
+ """Build the argument parser with all commands.
50
+
51
+ Returns:
52
+ Configured ArgumentParser instance.
53
+ """
54
+ from .. import __version__
55
+
56
+ parser = argparse.ArgumentParser(
57
+ description="QGIS Plugin Analyzer - A guardian for your PyQGIS code"
58
+ )
59
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
60
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
61
+
62
+ # Register all commands
63
+ for cmd in self.commands.values():
64
+ cmd_parser = subparsers.add_parser(cmd.name, help=cmd.help)
65
+ cmd.configure_parser(cmd_parser)
66
+
67
+ return parser
68
+
69
+ def _parse_args(
70
+ self, parser: argparse.ArgumentParser, argv: Optional[List[str]] = None
71
+ ) -> argparse.Namespace:
72
+ """Parse command-line arguments with legacy support.
73
+
74
+ Args:
75
+ parser: The argument parser.
76
+ argv: Optional argument list (defaults to sys.argv).
77
+
78
+ Returns:
79
+ Parsed arguments namespace.
80
+ """
81
+ if argv is None:
82
+ argv = sys.argv[1:]
83
+
84
+ # Legacy support: default to 'analyze' if first arg is a path
85
+ if argv and argv[0] not in self.commands and not argv[0].startswith("-"):
86
+ argv.insert(0, "analyze")
87
+
88
+ return parser.parse_args(argv)
89
+
90
+ def _setup_logging(self, args: argparse.Namespace) -> None:
91
+ """Setup logging based on command arguments.
92
+
93
+ Args:
94
+ args: Parsed command-line arguments.
95
+ """
96
+ output_dir = pathlib.Path(getattr(args, "output", "./analysis_results")).resolve()
97
+ output_dir.mkdir(parents=True, exist_ok=True)
98
+ setup_logger(output_dir)
99
+
100
+ def _execute_command(self, args: argparse.Namespace) -> int:
101
+ """Execute the selected command.
102
+
103
+ Args:
104
+ args: Parsed command-line arguments.
105
+
106
+ Returns:
107
+ Exit code from command execution.
108
+ """
109
+ if not args.command or args.command not in self.commands:
110
+ return 1
111
+
112
+ command = self.commands[args.command]
113
+ return command.execute(args)
114
+
115
+ def run(self, argv: Optional[List[str]] = None) -> int:
116
+ """Run the CLI application.
117
+
118
+ Args:
119
+ argv: Optional argument list (defaults to sys.argv).
120
+
121
+ Returns:
122
+ Exit code (0 for success, non-zero for failure).
123
+ """
124
+ parser = self._build_parser()
125
+
126
+ try:
127
+ args = self._parse_args(parser, argv)
128
+
129
+ if not args.command:
130
+ parser.print_help()
131
+ return 0
132
+
133
+ self._setup_logging(args)
134
+ return self._execute_command(args)
135
+
136
+ except KeyboardInterrupt:
137
+ logger.info("\n⏹️ Analysis interrupted.")
138
+ return 1
139
+ except FileNotFoundError as e:
140
+ logger.error(f"Error: File not found: {e}")
141
+ return 1
142
+ except ValueError as e:
143
+ logger.error(f"Error: {e}")
144
+ return 1
145
+ except Exception as e:
146
+ logger.critical(f"Critical Error: {e}", exc_info=True)
147
+ return 1
analyzer/cli/base.py ADDED
@@ -0,0 +1,93 @@
1
+ """Base command class for CLI commands."""
2
+
3
+ import argparse
4
+ import pathlib
5
+ from abc import ABC, abstractmethod
6
+ from typing import Optional
7
+
8
+
9
+ class BaseCommand(ABC):
10
+ """Abstract base class for CLI commands.
11
+
12
+ Each command encapsulates its own argument configuration and execution logic.
13
+ """
14
+
15
+ @property
16
+ @abstractmethod
17
+ def name(self) -> str:
18
+ """Command name as it appears in the CLI.
19
+
20
+ Returns:
21
+ The command name string.
22
+ """
23
+
24
+ @property
25
+ @abstractmethod
26
+ def help(self) -> str:
27
+ """Help text for the command.
28
+
29
+ Returns:
30
+ A brief description of what the command does.
31
+ """
32
+
33
+ @abstractmethod
34
+ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
35
+ """Configure command-specific arguments.
36
+
37
+ Args:
38
+ parser: The argument parser for this command.
39
+ """
40
+
41
+ @abstractmethod
42
+ def execute(self, args: argparse.Namespace) -> int:
43
+ """Execute the command.
44
+
45
+ Args:
46
+ args: Parsed command-line arguments.
47
+
48
+ Returns:
49
+ Exit code (0 for success, non-zero for failure).
50
+ """
51
+
52
+ def add_common_args(
53
+ self,
54
+ parser: argparse.ArgumentParser,
55
+ include_output: bool = True,
56
+ include_profile: bool = True,
57
+ ) -> None:
58
+ """Add common arguments shared across multiple commands.
59
+
60
+ Args:
61
+ parser: The argument parser to add arguments to.
62
+ include_output: Whether to include the --output argument.
63
+ include_profile: Whether to include the --profile argument.
64
+ """
65
+ if include_output:
66
+ parser.add_argument(
67
+ "-o",
68
+ "--output",
69
+ help="Output directory for reports",
70
+ default="./analysis_results",
71
+ )
72
+ if include_profile:
73
+ parser.add_argument(
74
+ "-p",
75
+ "--profile",
76
+ help="Configuration profile from pyproject.toml",
77
+ default="default",
78
+ )
79
+
80
+ def setup_output_dir(self, args: argparse.Namespace) -> Optional[pathlib.Path]:
81
+ """Setup and return the output directory if present in args.
82
+
83
+ Args:
84
+ args: Parsed command-line arguments.
85
+
86
+ Returns:
87
+ The resolved output directory path, or None if not applicable.
88
+ """
89
+ if hasattr(args, "output"):
90
+ output_dir = pathlib.Path(args.output).resolve()
91
+ output_dir.mkdir(parents=True, exist_ok=True)
92
+ return output_dir
93
+ return None
@@ -0,0 +1,19 @@
1
+ """CLI commands package."""
2
+
3
+ from .analyze import AnalyzeCommand
4
+ from .fix import FixCommand
5
+ from .init import InitCommand
6
+ from .list_rules import ListRulesCommand
7
+ from .security import SecurityCommand
8
+ from .summary import SummaryCommand
9
+ from .version import VersionCommand
10
+
11
+ __all__ = [
12
+ "AnalyzeCommand",
13
+ "SecurityCommand",
14
+ "FixCommand",
15
+ "ListRulesCommand",
16
+ "InitCommand",
17
+ "SummaryCommand",
18
+ "VersionCommand",
19
+ ]
@@ -0,0 +1,47 @@
1
+ """Analyze command implementation."""
2
+
3
+ import argparse
4
+
5
+ from ...commands import handle_analyze
6
+ from ..base import BaseCommand
7
+
8
+
9
+ class AnalyzeCommand(BaseCommand):
10
+ """Command to analyze an existing QGIS plugin."""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """Command name."""
15
+ return "analyze"
16
+
17
+ @property
18
+ def help(self) -> str:
19
+ """Command help text."""
20
+ return "Analyze an existing QGIS plugin"
21
+
22
+ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
23
+ """Configure analyze command arguments.
24
+
25
+ Args:
26
+ parser: The argument parser for this command.
27
+ """
28
+ parser.add_argument("project_path", help="Path to the QGIS project to analyze")
29
+ self.add_common_args(parser)
30
+ parser.add_argument(
31
+ "-r",
32
+ "--report",
33
+ action="store_true",
34
+ help="Generate detailed HTML/Markdown reports",
35
+ )
36
+
37
+ def execute(self, args: argparse.Namespace) -> int:
38
+ """Execute the analyze command.
39
+
40
+ Args:
41
+ args: Parsed command-line arguments.
42
+
43
+ Returns:
44
+ Exit code (0 for success).
45
+ """
46
+ handle_analyze(args)
47
+ return 0
@@ -0,0 +1,58 @@
1
+ """Fix command implementation."""
2
+
3
+ import argparse
4
+
5
+ from ...commands import handle_fix
6
+ from ..base import BaseCommand
7
+
8
+
9
+ class FixCommand(BaseCommand):
10
+ """Command to auto-fix common QGIS plugin issues."""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """Command name."""
15
+ return "fix"
16
+
17
+ @property
18
+ def help(self) -> str:
19
+ """Command help text."""
20
+ return "Auto-fix common QGIS plugin issues"
21
+
22
+ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
23
+ """Configure fix command arguments.
24
+
25
+ Args:
26
+ parser: The argument parser for this command.
27
+ """
28
+ parser.add_argument("path", type=str, help="Path to the QGIS plugin directory")
29
+ parser.add_argument(
30
+ "--dry-run",
31
+ action="store_true",
32
+ default=True,
33
+ help="Show proposed changes without applying (default: True)",
34
+ )
35
+ parser.add_argument("--apply", action="store_true", help="Apply fixes (disables dry-run)")
36
+ parser.add_argument(
37
+ "--auto-approve",
38
+ action="store_true",
39
+ help="Apply all fixes without confirmation",
40
+ )
41
+ self.add_common_args(parser, include_output=False)
42
+ parser.add_argument(
43
+ "--rules",
44
+ type=str,
45
+ help="Comma-separated list of rule IDs to fix",
46
+ )
47
+
48
+ def execute(self, args: argparse.Namespace) -> int:
49
+ """Execute the fix command.
50
+
51
+ Args:
52
+ args: Parsed command-line arguments.
53
+
54
+ Returns:
55
+ Exit code (0 for success).
56
+ """
57
+ handle_fix(args)
58
+ return 0
@@ -0,0 +1,41 @@
1
+ """Init command implementation."""
2
+
3
+ import argparse
4
+
5
+ from ...commands import handle_init
6
+ from ..base import BaseCommand
7
+
8
+
9
+ class InitCommand(BaseCommand):
10
+ """Command to initialize a new .analyzerignore with defaults."""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """Command name."""
15
+ return "init"
16
+
17
+ @property
18
+ def help(self) -> str:
19
+ """Command help text."""
20
+ return "Initialize a new .analyzerignore with defaults"
21
+
22
+ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
23
+ """Configure init command arguments.
24
+
25
+ Args:
26
+ parser: The argument parser for this command.
27
+ """
28
+ # No additional arguments needed
29
+ pass
30
+
31
+ def execute(self, args: argparse.Namespace) -> int:
32
+ """Execute the init command.
33
+
34
+ Args:
35
+ args: Parsed command-line arguments.
36
+
37
+ Returns:
38
+ Exit code (0 for success).
39
+ """
40
+ handle_init()
41
+ return 0
@@ -0,0 +1,41 @@
1
+ """List rules command implementation."""
2
+
3
+ import argparse
4
+
5
+ from ...commands import handle_list_rules
6
+ from ..base import BaseCommand
7
+
8
+
9
+ class ListRulesCommand(BaseCommand):
10
+ """Command to list all available QGIS audit rules."""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """Command name."""
15
+ return "list-rules"
16
+
17
+ @property
18
+ def help(self) -> str:
19
+ """Command help text."""
20
+ return "List all available QGIS audit rules"
21
+
22
+ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
23
+ """Configure list-rules command arguments.
24
+
25
+ Args:
26
+ parser: The argument parser for this command.
27
+ """
28
+ # No additional arguments needed
29
+ pass
30
+
31
+ def execute(self, args: argparse.Namespace) -> int:
32
+ """Execute the list-rules command.
33
+
34
+ Args:
35
+ args: Parsed command-line arguments.
36
+
37
+ Returns:
38
+ Exit code (0 for success).
39
+ """
40
+ handle_list_rules()
41
+ return 0
@@ -0,0 +1,46 @@
1
+ """Security command implementation."""
2
+
3
+ import argparse
4
+
5
+ from ...commands import handle_security
6
+ from ..base import BaseCommand
7
+
8
+
9
+ class SecurityCommand(BaseCommand):
10
+ """Command to run a focused security scan."""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """Command name."""
15
+ return "security"
16
+
17
+ @property
18
+ def help(self) -> str:
19
+ """Command help text."""
20
+ return "Run a focused security scan"
21
+
22
+ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
23
+ """Configure security command arguments.
24
+
25
+ Args:
26
+ parser: The argument parser for this command.
27
+ """
28
+ parser.add_argument("project_path", help="Path to the QGIS project to scan")
29
+ self.add_common_args(parser)
30
+ parser.add_argument(
31
+ "--deep",
32
+ action="store_true",
33
+ help="Run more intensive (but slower) security checks",
34
+ )
35
+
36
+ def execute(self, args: argparse.Namespace) -> int:
37
+ """Execute the security command.
38
+
39
+ Args:
40
+ args: Parsed command-line arguments.
41
+
42
+ Returns:
43
+ Exit code (0 for success).
44
+ """
45
+ handle_security(args)
46
+ return 0
@@ -0,0 +1,52 @@
1
+ """Summary command implementation."""
2
+
3
+ import argparse
4
+
5
+ from ...commands import handle_summary
6
+ from ..base import BaseCommand
7
+
8
+
9
+ class SummaryCommand(BaseCommand):
10
+ """Command to show a quick terminal summary of analysis results."""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """Command name."""
15
+ return "summary"
16
+
17
+ @property
18
+ def help(self) -> str:
19
+ """Command help text."""
20
+ return "Show a quick terminal summary of analysis results"
21
+
22
+ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
23
+ """Configure summary command arguments.
24
+
25
+ Args:
26
+ parser: The argument parser for this command.
27
+ """
28
+ parser.add_argument(
29
+ "-i",
30
+ "--input",
31
+ help="Path to the research JSON file",
32
+ default="analysis_results/project_context.json",
33
+ )
34
+ parser.add_argument(
35
+ "-b",
36
+ "--by",
37
+ choices=["total", "modules", "functions", "classes"],
38
+ default="total",
39
+ help="Granularity of the summary (default: total)",
40
+ )
41
+
42
+ def execute(self, args: argparse.Namespace) -> int:
43
+ """Execute the summary command.
44
+
45
+ Args:
46
+ args: Parsed command-line arguments.
47
+
48
+ Returns:
49
+ Exit code (0 for success).
50
+ """
51
+ handle_summary(args)
52
+ return 0
@@ -0,0 +1,41 @@
1
+ """Version command implementation."""
2
+
3
+ import argparse
4
+
5
+ from ... import __version__
6
+ from ..base import BaseCommand
7
+
8
+
9
+ class VersionCommand(BaseCommand):
10
+ """Command to show the current version of the analyzer."""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """Command name."""
15
+ return "version"
16
+
17
+ @property
18
+ def help(self) -> str:
19
+ """Command help text."""
20
+ return "Show the current version of the analyzer"
21
+
22
+ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
23
+ """Configure version command arguments.
24
+
25
+ Args:
26
+ parser: The argument parser for this command.
27
+ """
28
+ # No additional arguments needed
29
+ pass
30
+
31
+ def execute(self, args: argparse.Namespace) -> int:
32
+ """Execute the version command.
33
+
34
+ Args:
35
+ args: Parsed command-line arguments.
36
+
37
+ Returns:
38
+ Exit code (0 for success).
39
+ """
40
+ print(f"qgis-analyzer {__version__}")
41
+ return 0