qgis-plugin-analyzer 1.4.0__py3-none-any.whl → 1.5.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/visitors.py ADDED
@@ -0,0 +1,455 @@
1
+ """AST Visitors for QGIS Plugin Analysis."""
2
+
3
+ import ast
4
+ import re
5
+ from typing import Any, Dict, List, Optional, Set, cast
6
+
7
+ # Import to trigger registration of checks
8
+ from .rules.qgis_rules import I18N_METHODS
9
+ from .security_checker import SecurityContext, SecurityRegistry
10
+ from .utils.ast_utils import calculate_complexity
11
+
12
+
13
+ class QGISASTVisitor(ast.NodeVisitor):
14
+ """AST visitor to detect QGIS-specific issues."""
15
+
16
+ def __init__(self, rel_path: str, rules_config: Optional[Dict[str, Any]] = None) -> None:
17
+ """Initializes the AST visitor for a specific file.
18
+
19
+ Args:
20
+ rel_path: Relative path to the file being analyzed.
21
+ rules_config: Optional configuration for audit rules and severities.
22
+ """
23
+ self.rel_path = rel_path
24
+ self.issues: List[Dict[str, Any]] = []
25
+ self.rules_config = rules_config or {}
26
+ self.class_methods_stack: List[Set[str]] = []
27
+
28
+ # New metrics for research-based scoring
29
+ self.docstring_styles: List[str] = []
30
+ self.type_hint_stats = {
31
+ "total_parameters": 0,
32
+ "annotated_parameters": 0,
33
+ "has_return_hint": 0,
34
+ "total_functions": 0,
35
+ }
36
+ self.docstring_stats = {"total_public_items": 0, "has_docstring": 0}
37
+ self.i18n_methods = I18N_METHODS
38
+
39
+ def _should_report(self, rule_id: str) -> bool:
40
+ """Check if rule should be reported based on config."""
41
+ severity = self.rules_config.get(rule_id, "warning")
42
+ return bool(severity != "ignore")
43
+
44
+ def _get_severity(self, rule_id: str) -> str:
45
+ """Get configured severity for rule (maps to 'high', 'medium', 'low')."""
46
+ config_severity = self.rules_config.get(rule_id, "warning")
47
+ severity_map = {
48
+ "error": "high",
49
+ "warning": "medium",
50
+ "info": "low",
51
+ }
52
+ return severity_map.get(config_severity, "medium")
53
+
54
+ def _check_docstring_style(self, doc: Optional[str]) -> None:
55
+ """Identifies Google or NumPy docstring styles within a string."""
56
+ if not doc:
57
+ return
58
+ # Google: Args: or Returns: or Raises: as headers
59
+ if re.search(r"\n\s*(Args|Returns|Raises|Yields):\s*\n", doc):
60
+ self.docstring_styles.append("Google")
61
+ # NumPy: Underlined headers
62
+ elif re.search(r"\n(Parameters|Returns|Raises|Yields)\n\s*-{3,}", doc):
63
+ self.docstring_styles.append("NumPy")
64
+
65
+ def _report_issue(self, rule_id: str, line: int, message: str, code: str = "") -> None:
66
+ """Helper to report an issue if enabled."""
67
+ if self._should_report(rule_id):
68
+ self.issues.append(
69
+ {
70
+ "file": self.rel_path,
71
+ "line": line,
72
+ "type": rule_id,
73
+ "severity": self._get_severity(rule_id),
74
+ "message": message,
75
+ "code": code,
76
+ }
77
+ )
78
+
79
+ def visit_Module(self, node: ast.Module) -> None:
80
+ """Analyzes a module-level AST node."""
81
+ doc = ast.get_docstring(node)
82
+ self.docstring_stats["total_public_items"] += 1
83
+ if doc:
84
+ self.docstring_stats["has_docstring"] += 1
85
+ self._check_docstring_style(doc)
86
+ else:
87
+ self._report_issue(
88
+ "MISSING_DOCSTRING",
89
+ 1,
90
+ "Module is missing a docstring (PEP 257).",
91
+ f"Module: {self.rel_path}",
92
+ )
93
+ self.generic_visit(node)
94
+
95
+ def _check_import_name(self, name: str, node: ast.AST, code_snippet: str) -> None:
96
+ """Checks a single import name for violations."""
97
+ # 5. Detect QGIS_PROTECTED_MEMBER
98
+ if name.startswith("qgis._") and not name.startswith("qgis._3d"):
99
+ self._report_issue(
100
+ "QGIS_PROTECTED_MEMBER",
101
+ cast(Any, node).lineno,
102
+ f"Protected member import detected: '{name}'. Protected members are unstable.",
103
+ code_snippet,
104
+ )
105
+
106
+ # 6. Detect GDAL_DIRECT_IMPORT
107
+ if name == "gdal":
108
+ self._report_issue(
109
+ "GDAL_DIRECT_IMPORT",
110
+ cast(Any, node).lineno,
111
+ "Direct 'gdal' import detected. Use 'from osgeo import gdal'.",
112
+ code_snippet,
113
+ )
114
+
115
+ # Detect QGIS_LEGACY_IMPORT
116
+ if name.startswith(("PyQt4", "PyQt5")):
117
+ self._report_issue(
118
+ "QGIS_LEGACY_IMPORT",
119
+ cast(Any, node).lineno,
120
+ f"Legacy import detected: '{name}'. Use 'qgis.PyQt' for compatibility.",
121
+ code_snippet,
122
+ )
123
+
124
+ def visit_Import(self, node: ast.Import) -> None:
125
+ """Analyzes import nodes."""
126
+ for alias in node.names:
127
+ self._check_import_name(alias.name, node, ast.unparse(node))
128
+ self.generic_visit(node)
129
+
130
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
131
+ """Analyzes 'from import' nodes."""
132
+ if node.module:
133
+ self._check_import_name(node.module, node, ast.unparse(node))
134
+
135
+ # 7. Detect HEAVY_LOGIC_UI
136
+ heavy_libs = {"pandas", "numpy", "scipy", "sklearn", "matplotlib"}
137
+ is_ui_file = "gui" in self.rel_path.lower() or "ui" in self.rel_path.lower()
138
+ if is_ui_file and (
139
+ node.module in heavy_libs or node.module.split(".")[0] in heavy_libs
140
+ ):
141
+ self._report_issue(
142
+ "HEAVY_LOGIC_UI",
143
+ node.lineno,
144
+ f"Heavy dependency '{node.module}' detected in UI file. Move logic to core.",
145
+ ast.unparse(node),
146
+ )
147
+ self.generic_visit(node)
148
+
149
+ def _check_docstring_and_metrics(self, node: ast.FunctionDef) -> None:
150
+ """Checks docstrings and collects metrics."""
151
+ if not node.name.startswith("_") and node.name != "__init__":
152
+ doc = ast.get_docstring(node)
153
+ self.docstring_stats["total_public_items"] += 1
154
+ if doc:
155
+ self.docstring_stats["has_docstring"] += 1
156
+ self._check_docstring_style(doc)
157
+ else:
158
+ self._report_issue(
159
+ "MISSING_DOCSTRING",
160
+ node.lineno,
161
+ f"Public function '{node.name}' is missing a docstring.",
162
+ f"def {node.name}...",
163
+ )
164
+
165
+ def _check_type_hints(self, node: ast.FunctionDef) -> None:
166
+ """Checks for type hints."""
167
+ if node.name == "__init__":
168
+ return
169
+
170
+ self.type_hint_stats["total_functions"] += 1
171
+ params = [a for a in node.args.args if a.arg != "self" and a.arg != "cls"]
172
+ self.type_hint_stats["total_parameters"] += len(params)
173
+ annotated = [a for a in params if a.annotation]
174
+ self.type_hint_stats["annotated_parameters"] += len(annotated)
175
+ if node.returns:
176
+ self.type_hint_stats["has_return_hint"] += 1
177
+
178
+ if params and not annotated and not node.returns:
179
+ self._report_issue(
180
+ "MISSING_TYPE_HINTS",
181
+ node.lineno,
182
+ f"Function '{node.name}' has no type annotations.",
183
+ f"def {node.name}...",
184
+ )
185
+
186
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
187
+ """Analyzes function definitions."""
188
+ # IFACE_AS_ARGUMENT
189
+ for arg in node.args.args:
190
+ if arg.annotation and isinstance(arg.annotation, ast.Name):
191
+ if arg.annotation.id == "QgisInterface":
192
+ self._report_issue(
193
+ "IFACE_AS_ARGUMENT",
194
+ node.lineno,
195
+ f"Function '{node.name}' receives 'QgisInterface' as an argument. Use the global 'iface' or Singleton pattern.",
196
+ ast.unparse(node).split("\n")[0],
197
+ )
198
+
199
+ # HIGH_COMPLEXITY
200
+ complexity = calculate_complexity(node)
201
+ if complexity > 15:
202
+ self._report_issue(
203
+ "HIGH_COMPLEXITY",
204
+ node.lineno,
205
+ f"Function '{node.name}' is too complex (CC={complexity} > 15). Consider extracting methods.",
206
+ f"def {node.name}...",
207
+ )
208
+
209
+ self._check_docstring_and_metrics(node)
210
+ self._check_type_hints(node)
211
+ self.generic_visit(node)
212
+
213
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
214
+ """Analyzes class definitions."""
215
+ methods = {
216
+ item.name
217
+ for item in node.body
218
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef))
219
+ }
220
+ self.class_methods_stack.append(methods)
221
+
222
+ # MANDATORY_CLEANUP
223
+ has_init_gui = "initGui" in methods
224
+ has_unload = "unload" in methods
225
+
226
+ if has_init_gui and not has_unload:
227
+ self._report_issue(
228
+ "MANDATORY_CLEANUP",
229
+ node.lineno,
230
+ f"Class '{node.name}' implements 'initGui()' but is missing 'unload()'.",
231
+ f"class {node.name}...",
232
+ )
233
+
234
+ # Missing Docstring
235
+ if not node.name.startswith("_"):
236
+ doc = ast.get_docstring(node)
237
+ self.docstring_stats["total_public_items"] += 1
238
+ if doc:
239
+ self.docstring_stats["has_docstring"] += 1
240
+ self._check_docstring_style(doc)
241
+ else:
242
+ self._report_issue(
243
+ "MISSING_DOCSTRING",
244
+ node.lineno,
245
+ f"Public class '{node.name}' is missing a docstring.",
246
+ f"class {node.name}...",
247
+ )
248
+
249
+ self.generic_visit(node)
250
+ self.class_methods_stack.pop()
251
+
252
+ def visit_Call(self, node: ast.Call) -> None:
253
+ """Analyzes function calls."""
254
+ self._check_obsolete_api(node)
255
+ self._check_missing_i18n(node)
256
+ self._check_missing_slot(node)
257
+ self._check_unsafe_subprocess(node)
258
+ self._check_blocking_network(node)
259
+ self.generic_visit(node)
260
+
261
+ # ... Helper check methods (simplified) ...
262
+ def _check_obsolete_api(self, node: ast.Call) -> None:
263
+ if isinstance(node.func, ast.Attribute) and node.func.attr == "writeAsVectorFormat":
264
+ self._report_issue(
265
+ "OBSOLETE_API",
266
+ node.lineno,
267
+ "Obsolete writeAsVectorFormat() usage. Use writeAsVectorFormatV3().",
268
+ ast.unparse(node),
269
+ )
270
+
271
+ def _check_missing_i18n(self, node: ast.Call) -> None:
272
+ if isinstance(node.func, ast.Attribute) and node.func.attr in self.i18n_methods:
273
+ if (
274
+ node.args
275
+ and isinstance(node.args[0], ast.Constant)
276
+ and isinstance(node.args[0].value, str)
277
+ ):
278
+ val = node.args[0].value
279
+ if val.strip() and not val.startswith("%"):
280
+ self._report_issue(
281
+ "MISSING_I18N",
282
+ node.lineno,
283
+ f"Untranslated UI text string in '{node.func.attr}': '{val}'. Use self.tr().",
284
+ ast.unparse(node),
285
+ )
286
+
287
+ def _check_missing_slot(self, node: ast.Call) -> None:
288
+ if isinstance(node.func, ast.Attribute) and node.func.attr == "connect" and node.args:
289
+ arg = node.args[0]
290
+ if (
291
+ isinstance(arg, ast.Attribute)
292
+ and isinstance(arg.value, ast.Name)
293
+ and arg.value.id == "self"
294
+ ):
295
+ slot = arg.attr
296
+ if self.class_methods_stack and slot not in self.class_methods_stack[-1]:
297
+ self._report_issue(
298
+ "POTENTIAL_MISSING_SLOT",
299
+ node.lineno,
300
+ f"Connected slot 'self.{slot}' not found in class definitions.",
301
+ )
302
+
303
+ def _check_unsafe_subprocess(self, node: ast.Call) -> None:
304
+ is_subprocess = False
305
+ if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
306
+ if node.func.value.id == "subprocess" and node.func.attr in {
307
+ "run",
308
+ "call",
309
+ "Popen",
310
+ "check_call",
311
+ "check_output",
312
+ }:
313
+ is_subprocess = True
314
+
315
+ if not is_subprocess:
316
+ return
317
+
318
+ shell_true = False
319
+ for kw in node.keywords:
320
+ if kw.arg == "shell" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
321
+ shell_true = True
322
+ break
323
+
324
+ if shell_true:
325
+ self._report_issue(
326
+ "UNSAFE_SUBPROCESS",
327
+ node.lineno,
328
+ "Subprocess called with 'shell=True'.",
329
+ ast.unparse(node),
330
+ )
331
+ return
332
+
333
+ if node.args:
334
+ cmd_arg = node.args[0]
335
+ if isinstance(cmd_arg, (ast.JoinedStr, ast.BinOp)):
336
+ self._report_issue(
337
+ "UNSAFE_SUBPROCESS",
338
+ node.lineno,
339
+ "Possible unquoted variable injection.",
340
+ ast.unparse(node),
341
+ )
342
+
343
+ def _check_blocking_network(self, node: ast.Call) -> None:
344
+ is_network = False
345
+ if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
346
+ if node.func.value.id == "requests" and node.func.attr in {
347
+ "get",
348
+ "post",
349
+ "put",
350
+ "delete",
351
+ "patch",
352
+ }:
353
+ is_network = True
354
+
355
+ if not is_network:
356
+ # Heuristic for urlopen
357
+ attr_chain = []
358
+ curr = node.func
359
+ while isinstance(curr, ast.Attribute):
360
+ attr_chain.append(curr.attr)
361
+ curr = curr.value
362
+ if isinstance(curr, ast.Name):
363
+ attr_chain.append(curr.id)
364
+ if attr_chain == ["urlopen", "request", "urllib"]:
365
+ is_network = True
366
+
367
+ if is_network:
368
+ is_ui_file = any(
369
+ kw in self.rel_path.lower() for kw in ["gui", "ui", "dialog", "widget"]
370
+ )
371
+ if is_ui_file:
372
+ self._report_issue(
373
+ "BLOCKING_NETWORK_CALL",
374
+ node.lineno,
375
+ "Synchronous network call detected in UI file.",
376
+ ast.unparse(node),
377
+ )
378
+
379
+ def visit_For(self, node: ast.For) -> None:
380
+ """Analyzes loop nodes."""
381
+ # SPATIAL_INDEX check
382
+ if (
383
+ isinstance(node.iter, ast.Call)
384
+ and isinstance(node.iter.func, ast.Attribute)
385
+ and node.iter.func.attr == "getFeatures"
386
+ ):
387
+ warn = False
388
+ if not node.iter.args:
389
+ warn = True
390
+ elif len(node.iter.args) == 1:
391
+ arg = node.iter.args[0]
392
+ if (
393
+ isinstance(arg, ast.Call)
394
+ and isinstance(arg.func, ast.Name)
395
+ and arg.func.id == "QgsFeatureRequest"
396
+ ):
397
+ if not arg.args and not arg.keywords:
398
+ warn = True
399
+
400
+ if warn:
401
+ self._report_issue(
402
+ "SPATIAL_INDEX",
403
+ node.lineno,
404
+ "Iteration over features with getFeatures() and no filter.",
405
+ ast.unparse(node.iter),
406
+ )
407
+
408
+ # NON_PYTHONIC_LOOP
409
+ for body_node in ast.walk(node):
410
+ if isinstance(body_node, ast.AugAssign) and isinstance(body_node.op, ast.Add):
411
+ if (
412
+ isinstance(body_node.target, ast.Name)
413
+ and isinstance(body_node.value, ast.Constant)
414
+ and body_node.value.value == 1
415
+ ):
416
+ self._report_issue(
417
+ "NON_PYTHONIC_LOOP",
418
+ body_node.lineno,
419
+ f"Manual counter '{body_node.target.id} += 1' detected inside loop.",
420
+ ast.unparse(body_node),
421
+ )
422
+
423
+ self.generic_visit(node)
424
+
425
+
426
+ class QGISSecurityVisitor(ast.NodeVisitor):
427
+ """AST visitor focused on security vulnerabilities (Bandit-inspired)."""
428
+
429
+ def __init__(self, rel_path: str):
430
+ self.rel_path = rel_path
431
+ self.findings: List[Dict[str, Any]] = []
432
+
433
+ def visit(self, node: ast.AST):
434
+ """Dispatches security checks for the current node."""
435
+ checks = SecurityRegistry.get_checks_for_node(type(node))
436
+ context = SecurityContext(node, self.rel_path)
437
+
438
+ for check_func in checks:
439
+ finding = check_func(context)
440
+ if finding:
441
+ self.findings.append(
442
+ {
443
+ "file": self.rel_path,
444
+ "line": finding.line,
445
+ "type": finding.id,
446
+ "severity": finding.severity.lower(),
447
+ "message": finding.message,
448
+ "code": finding.code_snippet,
449
+ "confidence": finding.confidence.lower()
450
+ if hasattr(finding, "confidence")
451
+ else "medium",
452
+ }
453
+ )
454
+
455
+ super().generic_visit(node)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qgis-plugin-analyzer
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: A professional static analysis tool for QGIS (PyQGIS) plugins
5
5
  Author-email: geociencio <juanbernales@gmail.com>
6
6
  License: GPL-3.0-or-later
@@ -34,13 +34,16 @@ Dynamic: license-file
34
34
  ![License](https://img.shields.io/badge/License-GPLv3-blue.svg)
35
35
  ![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)
36
36
  ![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?logo=git)
37
- ![Quality Score](https://img.shields.io/badge/Module%20Stability-55.6%2F100-yellow)
38
- ![Maintainability](https://img.shields.io/badge/Maintainability-100.0%2F100-brightgreen)
37
+ ![Quality Score](https://img.shields.io/badge/Module%20Stability-92.3%2F100-brightgreen)
38
+ ![Maintainability](https://img.shields.io/badge/Maintainability-84.1%2F100-green)
39
+ ![Security Score](https://img.shields.io/badge/Security--Bandit-98.7%2F100-brightgreen)
39
40
 
40
41
  The **QGIS Plugin Analyzer** is a static analysis tool designed specifically for QGIS (PyQGIS) plugin developers. Its goal is to elevate plugin quality by ensuring they follow community best practices and are optimized for AI-assisted development.
41
42
 
42
43
  ## ✨ Main Features
43
44
 
45
+ - **Security Core (Bandit-inspired)**: Professional vulnerability scanning detecting `eval`, `exec`, shell injections, and SQL injection risks.
46
+ - **Deep Entropy Secret Scanner**: Detects hardcoded API keys, passwords, and sensitive tokens using regex and information entropy.
44
47
  - **High-Performance Engine**: Parallel analysis powered by `ProcessPoolExecutor` for ultra-fast execution on multi-core systems.
45
48
  - **Project Auto-Detection**: Intelligently distinguishes between official QGIS Plugins and Generic Python Projects, tailoring validation logic accordingly.
46
49
  - **Advanced Ignore Engine**: Robust `.analyzerignore` support with non-anchored patterns and smart default excludes (`.venv`, `build`, etc.).
@@ -63,9 +66,8 @@ The **QGIS Plugin Analyzer** is a static analysis tool designed specifically for
63
66
  | **QGIS-Specific Rules**| ✅ (Precise AST) | ✅ (Regex/AST) | ❌ | ✅ |
64
67
  | **Interactive Auto-Fix**| ✅ | ❌ | ❌ | ❌ |
65
68
  | **Semantic Analysis** | ✅ | ❌ | ❌ | ❌ |
66
- | **Compliance Checks** | ✅ | ❌ | ❌ | |
67
- | **i18n / API Audit** | ✅ | ❌ | ❌ | |
68
- | **Architecture Audit** | ✅ (UI/Core) | ❌ | ❌ | ❌ |
69
+ | **Security Audit** | ✅ (Bandit-style) | ❌ | ❌ | |
70
+ | **Secret Scanning** | ✅ (Entropy) | ❌ | ❌ | |
69
71
  | **HTML/MD Reports** | ✅ | ❌ | ❌ | ❌ |
70
72
  | **AI Context Gen** | ✅ (Project Brain) | ❌ | ❌ | ❌ |
71
73
 
@@ -129,7 +131,7 @@ You can run `qgis-plugin-analyzer` automatically before every commit to ensure q
129
131
 
130
132
  ```yaml
131
133
  - repo: https://github.com/geociencio/qgis-plugin-analyzer
132
- rev: v1.4.0 # Use the latest tag
134
+ rev: v1.5.0 # Use the latest tag
133
135
  hooks:
134
136
  - id: qgis-plugin-analyzer
135
137
  ```
@@ -210,6 +212,17 @@ Shows a professional, color-coded summary of findings directly in your terminal.
210
212
  | `-b`, `--by` | Granularity of the summary: `total`, `modules`, `functions`, `classes`. | `total` |
211
213
  | `-i`, `--input` | Path to the `project_context.json` file to summarize. | `analysis_results/project_context.json` |
212
214
 
215
+ ### `qgis-analyzer security`
216
+ Performs a focused security scan on a file or directory.
217
+
218
+ | Argument | Description | Default |
219
+ | :--- | :--- | :--- |
220
+ | `path` | **(Required)** Path to the file or directory to scan. | N/A |
221
+ | `-p`, `--profile`| Configuration profile. | `default` |
222
+
223
+ ### `qgis-analyzer version`
224
+ Shows the current version of the analyzer.
225
+
213
226
  **Example:**
214
227
  ```bash
215
228
  # Executive summary
@@ -1,30 +1,35 @@
1
1
  __init__.py,sha256=m27nXDOFpStOzfg3Qqeaw7yj1wNmpnOH7xk5svlCprA,1185
2
- analyzer/__init__.py,sha256=m27nXDOFpStOzfg3Qqeaw7yj1wNmpnOH7xk5svlCprA,1185
3
- analyzer/cli.py,sha256=x6ZfhtTyJbLDSA8eNlKpXdeHc0nvvp0jdN40mYFHXPs,10488
4
- analyzer/engine.py,sha256=rbJtEQj1_Qr69Z3Uzx3c97iCw1BkS8A4gQv9WnV0Egk,22241
2
+ analyzer/__init__.py,sha256=4cVdHyVkLCwrgmcijSYhVDiKxMJJFTeK4SOJXNXWJwc,1129
3
+ analyzer/cli.py,sha256=x3mEG8Y5m18RLswXUkaky_QoY5CR17DsXwyg-bSnvZ0,7077
4
+ analyzer/commands.py,sha256=cjXVtipPutYR7NCAzg9uxB1NZKkZ20GQpRseSPJER2A,5127
5
+ analyzer/engine.py,sha256=ls9WrjH5eFHt8xghQALGOmRgb_-VvBmnPuGC1ZCAUG8,24431
5
6
  analyzer/fixer.py,sha256=6CujODr4pJgdR2XuJoBOCrQxBHjJfvVsBmg4YZdKJAM,10653
6
- analyzer/scanner.py,sha256=o4pvtUnYn2ZYo5OhEwswm7BEeIA61XrosmrhS5Al8SI,34260
7
+ analyzer/scanner.py,sha256=3NUsEumloeND_Wd1V-fOnn3GklXDpIh31gN4Hhuf9AU,8154
8
+ analyzer/secrets.py,sha256=Ml-hRSFgLlAsi6KeD2V53OuQ1E8qWE_65yzDm_RD3AE,2899
9
+ analyzer/security_checker.py,sha256=YXQeIvtrtwvqHm5xGW8zXNrPycdsLyavCnxFOltf8Rs,2839
10
+ analyzer/security_rules.py,sha256=x95EweqgTOFNcM6In2aA37qOeqsv1PBYkCsRmwhpwGw,5182
7
11
  analyzer/semantic.py,sha256=cVmSiTmI-b4DFZ5Q6kZUdqd6ZPH92pJmNAE60MIiZRI,8192
8
12
  analyzer/transformers.py,sha256=tFmelReM8ILpFjMBSDIDMCXPSZlD9t8FD7lTXVgfOgA,7103
9
13
  analyzer/validators.py,sha256=5JPwQYQ4KKThZchr34nV06-VToFhgBOtIp7lNhO5WTk,8344
14
+ analyzer/visitors.py,sha256=DRwqJal_pTtXs7zNq7iSKc7EvyDhTaXRAu1lvm05St0,17448
10
15
  analyzer/models/__init__.py,sha256=NlDgdV1i9CoWJBgiENoJQuHnYCOzO6U_ui3wHOeT7xI,160
11
16
  analyzer/models/analysis_models.py,sha256=WXsYgbjD4ngBoyaY-YkwMMl2Jpbn96hue6zuSERZRb0,2362
12
17
  analyzer/reporters/__init__.py,sha256=fVsu2gCuBdDd_STD97Jku8otcO3j7NZrxxd_pGzf6Sw,280
13
18
  analyzer/reporters/html_reporter.py,sha256=j64KkjiRwwRBzIPZE7bBJGsbJEYIoSnZeho63tI4c5A,14919
14
- analyzer/reporters/markdown_reporter.py,sha256=sc0zBM4_hJmLE8tZF8lV_v7VLRlNSEhWqSivFZMQKac,8074
15
- analyzer/reporters/summary_reporter.py,sha256=v4XfmcE027fCIWmS5N0odCx-NdziWuCefBIacRhi8XA,7237
19
+ analyzer/reporters/markdown_reporter.py,sha256=gNTP2K8yFlbTMceuxxTsxGyAkAUUxyGQD0KMyHzaQ2Q,9568
20
+ analyzer/reporters/summary_reporter.py,sha256=di_fNUcNgLPtSNQHuCj4J_jGVzQUZ5aqw_-BHhnhzBo,9572
16
21
  analyzer/rules/__init__.py,sha256=UNZoY89SFQoUKYGj_rjwvv8md6vXSSM2pGPV6MqC2Fg,261
17
22
  analyzer/rules/modernization_rules.py,sha256=IJw8c6v7Q_5z3xF9Dh0eBrproRFZFtIOyWcArAsSv18,1071
18
- analyzer/rules/qgis_rules.py,sha256=N_1RxWrckOZWzcAMPI-07vLQPazO6MM7-LF69_NFMy8,2548
23
+ analyzer/rules/qgis_rules.py,sha256=vpFkdC3RnlkcSHqJB_Ec1wYLn8qV8xMjXZCgitslRSc,2578
19
24
  analyzer/utils/__init__.py,sha256=7abAIymmrA8FlzqsM8JfKXnfx3HjX0r-8KdvmgKMRQA,1006
20
25
  analyzer/utils/ast_utils.py,sha256=LZ9NLrbJlhFGM7LsX6YNexOi_avJ0Sxzf8GdZHvoXK4,3831
21
26
  analyzer/utils/config_utils.py,sha256=reNyUonmik8Tpwx0RbaslsiienIqPDeiXBfh4Pv23_E,4690
22
27
  analyzer/utils/logging_utils.py,sha256=V0z7XeF46tj2oI247j2fRRafClbtMDVscVmg2JFYHV4,1257
23
28
  analyzer/utils/path_utils.py,sha256=BkffM1TRKAMPCzLx49rQzmYHYGbPwlI41LCK5QoN2oU,4097
24
29
  analyzer/utils/performance_utils.py,sha256=M3mfMVrOvyfgQiCQB0R5IkkBnAx-L9er4fGR9UL5E-0,4253
25
- qgis_plugin_analyzer-1.4.0.dist-info/licenses/LICENSE,sha256=bMxViCXCE9h3bzmw6oUvy4R_CvzOrV3aYhIvjBPx20A,35091
26
- qgis_plugin_analyzer-1.4.0.dist-info/METADATA,sha256=nwhnhEsDydRGcYiq-OkdbqxYn3JptRCRcH90_WvloLg,12906
27
- qgis_plugin_analyzer-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- qgis_plugin_analyzer-1.4.0.dist-info/entry_points.txt,sha256=1dwiqkZC4hGYExrOgyhZkxv7CbClSZqJkVk42nlw1IY,52
29
- qgis_plugin_analyzer-1.4.0.dist-info/top_level.txt,sha256=i9U1DboCuTuaYpS-5GcgUeKIlSI00PoxYtnrfQWD8wo,18
30
- qgis_plugin_analyzer-1.4.0.dist-info/RECORD,,
30
+ qgis_plugin_analyzer-1.5.0.dist-info/licenses/LICENSE,sha256=bMxViCXCE9h3bzmw6oUvy4R_CvzOrV3aYhIvjBPx20A,35091
31
+ qgis_plugin_analyzer-1.5.0.dist-info/METADATA,sha256=x6KPAK7Rgr1K9lNCquZioxhZk9KHtQswmOF8RhRGq8g,13583
32
+ qgis_plugin_analyzer-1.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
33
+ qgis_plugin_analyzer-1.5.0.dist-info/entry_points.txt,sha256=1dwiqkZC4hGYExrOgyhZkxv7CbClSZqJkVk42nlw1IY,52
34
+ qgis_plugin_analyzer-1.5.0.dist-info/top_level.txt,sha256=i9U1DboCuTuaYpS-5GcgUeKIlSI00PoxYtnrfQWD8wo,18
35
+ qgis_plugin_analyzer-1.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5