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.
- analyzer/cli/__init__.py +14 -0
- analyzer/cli/app.py +147 -0
- analyzer/cli/base.py +93 -0
- analyzer/cli/commands/__init__.py +19 -0
- analyzer/cli/commands/analyze.py +47 -0
- analyzer/cli/commands/fix.py +58 -0
- analyzer/cli/commands/init.py +41 -0
- analyzer/cli/commands/list_rules.py +41 -0
- analyzer/cli/commands/security.py +46 -0
- analyzer/cli/commands/summary.py +52 -0
- analyzer/cli/commands/version.py +41 -0
- analyzer/cli.py +4 -184
- analyzer/commands.py +7 -7
- analyzer/engine.py +421 -238
- analyzer/fixer.py +206 -130
- analyzer/reporters/markdown_reporter.py +48 -15
- analyzer/reporters/summary_reporter.py +193 -80
- analyzer/scanner.py +218 -138
- analyzer/transformers.py +29 -8
- analyzer/utils/__init__.py +2 -0
- analyzer/utils/path_utils.py +53 -1
- analyzer/validators.py +90 -55
- analyzer/visitors/__init__.py +19 -0
- analyzer/visitors/base.py +75 -0
- analyzer/visitors/composite_visitor.py +73 -0
- analyzer/visitors/imports_visitor.py +85 -0
- analyzer/visitors/metrics_visitor.py +158 -0
- analyzer/visitors/security_visitor.py +52 -0
- analyzer/visitors/standards_visitor.py +284 -0
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/METADATA +16 -7
- qgis_plugin_analyzer-1.6.0.dist-info/RECORD +52 -0
- analyzer/visitors.py +0 -455
- qgis_plugin_analyzer-1.5.0.dist-info/RECORD +0 -35
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/WHEEL +0 -0
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/entry_points.txt +0 -0
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/top_level.txt +0 -0
analyzer/visitors.py
DELETED
|
@@ -1,455 +0,0 @@
|
|
|
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,35 +0,0 @@
|
|
|
1
|
-
__init__.py,sha256=m27nXDOFpStOzfg3Qqeaw7yj1wNmpnOH7xk5svlCprA,1185
|
|
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
|
|
6
|
-
analyzer/fixer.py,sha256=6CujODr4pJgdR2XuJoBOCrQxBHjJfvVsBmg4YZdKJAM,10653
|
|
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
|
|
11
|
-
analyzer/semantic.py,sha256=cVmSiTmI-b4DFZ5Q6kZUdqd6ZPH92pJmNAE60MIiZRI,8192
|
|
12
|
-
analyzer/transformers.py,sha256=tFmelReM8ILpFjMBSDIDMCXPSZlD9t8FD7lTXVgfOgA,7103
|
|
13
|
-
analyzer/validators.py,sha256=5JPwQYQ4KKThZchr34nV06-VToFhgBOtIp7lNhO5WTk,8344
|
|
14
|
-
analyzer/visitors.py,sha256=DRwqJal_pTtXs7zNq7iSKc7EvyDhTaXRAu1lvm05St0,17448
|
|
15
|
-
analyzer/models/__init__.py,sha256=NlDgdV1i9CoWJBgiENoJQuHnYCOzO6U_ui3wHOeT7xI,160
|
|
16
|
-
analyzer/models/analysis_models.py,sha256=WXsYgbjD4ngBoyaY-YkwMMl2Jpbn96hue6zuSERZRb0,2362
|
|
17
|
-
analyzer/reporters/__init__.py,sha256=fVsu2gCuBdDd_STD97Jku8otcO3j7NZrxxd_pGzf6Sw,280
|
|
18
|
-
analyzer/reporters/html_reporter.py,sha256=j64KkjiRwwRBzIPZE7bBJGsbJEYIoSnZeho63tI4c5A,14919
|
|
19
|
-
analyzer/reporters/markdown_reporter.py,sha256=gNTP2K8yFlbTMceuxxTsxGyAkAUUxyGQD0KMyHzaQ2Q,9568
|
|
20
|
-
analyzer/reporters/summary_reporter.py,sha256=di_fNUcNgLPtSNQHuCj4J_jGVzQUZ5aqw_-BHhnhzBo,9572
|
|
21
|
-
analyzer/rules/__init__.py,sha256=UNZoY89SFQoUKYGj_rjwvv8md6vXSSM2pGPV6MqC2Fg,261
|
|
22
|
-
analyzer/rules/modernization_rules.py,sha256=IJw8c6v7Q_5z3xF9Dh0eBrproRFZFtIOyWcArAsSv18,1071
|
|
23
|
-
analyzer/rules/qgis_rules.py,sha256=vpFkdC3RnlkcSHqJB_Ec1wYLn8qV8xMjXZCgitslRSc,2578
|
|
24
|
-
analyzer/utils/__init__.py,sha256=7abAIymmrA8FlzqsM8JfKXnfx3HjX0r-8KdvmgKMRQA,1006
|
|
25
|
-
analyzer/utils/ast_utils.py,sha256=LZ9NLrbJlhFGM7LsX6YNexOi_avJ0Sxzf8GdZHvoXK4,3831
|
|
26
|
-
analyzer/utils/config_utils.py,sha256=reNyUonmik8Tpwx0RbaslsiienIqPDeiXBfh4Pv23_E,4690
|
|
27
|
-
analyzer/utils/logging_utils.py,sha256=V0z7XeF46tj2oI247j2fRRafClbtMDVscVmg2JFYHV4,1257
|
|
28
|
-
analyzer/utils/path_utils.py,sha256=BkffM1TRKAMPCzLx49rQzmYHYGbPwlI41LCK5QoN2oU,4097
|
|
29
|
-
analyzer/utils/performance_utils.py,sha256=M3mfMVrOvyfgQiCQB0R5IkkBnAx-L9er4fGR9UL5E-0,4253
|
|
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,,
|
|
File without changes
|
{qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{qgis_plugin_analyzer-1.5.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|