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/fixer.py
CHANGED
|
@@ -7,18 +7,38 @@
|
|
|
7
7
|
import difflib
|
|
8
8
|
import pathlib
|
|
9
9
|
import subprocess
|
|
10
|
-
import
|
|
11
|
-
from abc import ABC, abstractmethod
|
|
12
|
-
from typing import Any, Dict, List
|
|
10
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
13
11
|
|
|
14
12
|
from .transformers import (
|
|
15
13
|
GDALImportTransformer,
|
|
16
14
|
I18nTransformer,
|
|
17
15
|
LegacyImportTransformer,
|
|
18
16
|
PrintToLogTransformer,
|
|
19
|
-
|
|
17
|
+
apply_transformation_to_content,
|
|
20
18
|
)
|
|
21
19
|
|
|
20
|
+
# --- Types ---
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FixContext(TypedDict):
|
|
24
|
+
"""Context information for a fix handler."""
|
|
25
|
+
|
|
26
|
+
project_path: pathlib.Path
|
|
27
|
+
file_path: pathlib.Path
|
|
28
|
+
issue: Dict[str, Any]
|
|
29
|
+
content: str # Original file content
|
|
30
|
+
dry_run: bool
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FixHandlerResult(TypedDict):
|
|
34
|
+
"""Result of a fix execution."""
|
|
35
|
+
|
|
36
|
+
applied: bool
|
|
37
|
+
message: str
|
|
38
|
+
new_content: Optional[str] # Transformed code if applied
|
|
39
|
+
diff: Optional[str]
|
|
40
|
+
error: Optional[str]
|
|
41
|
+
|
|
22
42
|
|
|
23
43
|
def check_git_status(project_path: pathlib.Path) -> bool:
|
|
24
44
|
"""Checks if the Git working directory is clean.
|
|
@@ -76,80 +96,123 @@ def show_diff(file_path: pathlib.Path, original_content: str, new_content: str)
|
|
|
76
96
|
print(" " + "─" * 60)
|
|
77
97
|
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
"""Abstract base class for all auto-fix strategies."""
|
|
81
|
-
|
|
82
|
-
@abstractmethod
|
|
83
|
-
def can_fix(self, issue: Dict[str, Any]) -> bool:
|
|
84
|
-
"""Returns True if this strategy can fix the given issue."""
|
|
85
|
-
pass
|
|
99
|
+
# --- Fix Registry ---
|
|
86
100
|
|
|
87
|
-
@abstractmethod
|
|
88
|
-
def apply_fix(self, file_path: pathlib.Path, issue: Dict[str, Any]) -> bool:
|
|
89
|
-
"""Applies the fix to the file. Returns True if successful."""
|
|
90
|
-
pass
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"""Returns a human-readable description of the fix."""
|
|
95
|
-
pass
|
|
102
|
+
class FixRegistry:
|
|
103
|
+
"""Registry for managing and discovering fix handlers."""
|
|
96
104
|
|
|
105
|
+
def __init__(self) -> None:
|
|
106
|
+
self._handlers: Dict[str, Any] = {}
|
|
97
107
|
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
def register(self, issue_type: str):
|
|
109
|
+
"""Decorator to register a fix handler for a specific issue type."""
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
def decorator(func):
|
|
112
|
+
self._handlers[issue_type] = func
|
|
113
|
+
return func
|
|
103
114
|
|
|
104
|
-
|
|
105
|
-
transformer = GDALImportTransformer()
|
|
106
|
-
return apply_transformation(file_path, transformer)
|
|
115
|
+
return decorator
|
|
107
116
|
|
|
108
|
-
def
|
|
109
|
-
|
|
117
|
+
def get_handler(self, issue_type: str) -> Optional[Any]:
|
|
118
|
+
"""Retrieves a handler for a given issue type."""
|
|
119
|
+
return self._handlers.get(issue_type)
|
|
110
120
|
|
|
121
|
+
def get_all_handlers(self) -> List[Any]:
|
|
122
|
+
"""Returns all registered handlers."""
|
|
123
|
+
return list(self._handlers.values())
|
|
111
124
|
|
|
112
|
-
class LegacyImportFixer(FixStrategy):
|
|
113
|
-
"""Fixes PyQt4/PyQt5 imports."""
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
return issue.get("type") == "QGIS_LEGACY_IMPORT"
|
|
126
|
+
registry = FixRegistry()
|
|
117
127
|
|
|
118
|
-
def apply_fix(self, file_path: pathlib.Path, issue: Dict[str, Any]) -> bool:
|
|
119
|
-
transformer = LegacyImportTransformer()
|
|
120
|
-
return apply_transformation(file_path, transformer)
|
|
121
128
|
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
def create_ast_handler(issue_type: str, transformer_cls: Any, description_msg: str) -> Any:
|
|
130
|
+
"""Factory function to create standard AST-based handlers.
|
|
124
131
|
|
|
132
|
+
Args:
|
|
133
|
+
issue_type: The issue identifier this handler targets.
|
|
134
|
+
transformer_cls: The AST NodeTransformer class to instantiate.
|
|
135
|
+
description_msg: Human-readable description of the fix.
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def can_fix(self, issue: Dict[str, Any]) -> bool:
|
|
130
|
-
# This would need a new rule type in scanner.py
|
|
131
|
-
return issue.get("type") == "PRINT_STATEMENT"
|
|
132
|
-
|
|
133
|
-
def apply_fix(self, file_path: pathlib.Path, issue: Dict[str, Any]) -> bool:
|
|
134
|
-
transformer = PrintToLogTransformer()
|
|
135
|
-
return apply_transformation(file_path, transformer)
|
|
136
|
-
|
|
137
|
-
def get_description(self, issue: Dict[str, Any]) -> str:
|
|
138
|
-
return "Replace print() with QgsMessageLog.logMessage()"
|
|
139
|
-
|
|
137
|
+
Returns:
|
|
138
|
+
A handler function compatible with FixRegistry.
|
|
139
|
+
"""
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
def handler(ctx: FixContext) -> FixHandlerResult:
|
|
142
|
+
if ctx["issue"].get("type") != issue_type:
|
|
143
|
+
return {
|
|
144
|
+
"applied": False,
|
|
145
|
+
"message": "",
|
|
146
|
+
"new_content": None,
|
|
147
|
+
"diff": None,
|
|
148
|
+
"error": None,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if ctx["dry_run"]:
|
|
152
|
+
return {
|
|
153
|
+
"applied": True,
|
|
154
|
+
"message": description_msg,
|
|
155
|
+
"new_content": None, # Not computed in dry-run
|
|
156
|
+
"diff": None,
|
|
157
|
+
"error": None,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
transformer = transformer_cls()
|
|
161
|
+
new_code = apply_transformation_to_content(ctx["content"], transformer)
|
|
162
|
+
|
|
163
|
+
if new_code is not None:
|
|
164
|
+
return {
|
|
165
|
+
"applied": True,
|
|
166
|
+
"message": description_msg,
|
|
167
|
+
"new_content": new_code,
|
|
168
|
+
"diff": None,
|
|
169
|
+
"error": None,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"applied": False,
|
|
174
|
+
"message": "",
|
|
175
|
+
"new_content": None,
|
|
176
|
+
"diff": None,
|
|
177
|
+
"error": None,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return handler
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# --- Fix Handlers Registration ---
|
|
184
|
+
|
|
185
|
+
registry.register("GDAL_DIRECT_IMPORT")(
|
|
186
|
+
create_ast_handler(
|
|
187
|
+
"GDAL_DIRECT_IMPORT",
|
|
188
|
+
GDALImportTransformer,
|
|
189
|
+
"Replace 'import gdal' with 'from osgeo import gdal'",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
143
192
|
|
|
144
|
-
|
|
145
|
-
|
|
193
|
+
registry.register("QGIS_LEGACY_IMPORT")(
|
|
194
|
+
create_ast_handler(
|
|
195
|
+
"QGIS_LEGACY_IMPORT",
|
|
196
|
+
LegacyImportTransformer,
|
|
197
|
+
"Replace PyQt4/PyQt5 imports with qgis.PyQt",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
146
200
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
201
|
+
registry.register("PRINT_STATEMENT")(
|
|
202
|
+
create_ast_handler(
|
|
203
|
+
"PRINT_STATEMENT",
|
|
204
|
+
PrintToLogTransformer,
|
|
205
|
+
"Replace print() with QgsMessageLog.logMessage()",
|
|
206
|
+
)
|
|
207
|
+
)
|
|
150
208
|
|
|
151
|
-
|
|
152
|
-
|
|
209
|
+
registry.register("MISSING_I18N")(
|
|
210
|
+
create_ast_handler(
|
|
211
|
+
"MISSING_I18N",
|
|
212
|
+
I18nTransformer,
|
|
213
|
+
"Wrap hardcoded string in self.tr()",
|
|
214
|
+
)
|
|
215
|
+
)
|
|
153
216
|
|
|
154
217
|
|
|
155
218
|
class AutoFixer:
|
|
@@ -158,7 +221,7 @@ class AutoFixer:
|
|
|
158
221
|
Attributes:
|
|
159
222
|
project_path: Root path of the project.
|
|
160
223
|
dry_run: If True, changes are proposed but not written.
|
|
161
|
-
|
|
224
|
+
registry: FixRegistry instance containing handlers.
|
|
162
225
|
"""
|
|
163
226
|
|
|
164
227
|
def __init__(self, project_path: pathlib.Path, dry_run: bool = True) -> None:
|
|
@@ -170,12 +233,7 @@ class AutoFixer:
|
|
|
170
233
|
"""
|
|
171
234
|
self.project_path = project_path
|
|
172
235
|
self.dry_run = dry_run
|
|
173
|
-
self.
|
|
174
|
-
GDALImportFixer(),
|
|
175
|
-
LegacyImportFixer(),
|
|
176
|
-
PrintToLogFixer(),
|
|
177
|
-
I18nFixer(),
|
|
178
|
-
]
|
|
236
|
+
self.registry = registry
|
|
179
237
|
|
|
180
238
|
def get_fixable_issues(self, issues: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
181
239
|
"""Filters a list of issues to identify those that can be auto-fixed.
|
|
@@ -184,17 +242,34 @@ class AutoFixer:
|
|
|
184
242
|
issues: A list of issue dictionaries.
|
|
185
243
|
|
|
186
244
|
Returns:
|
|
187
|
-
A list of fixable issues, each enriched with a '
|
|
245
|
+
A list of fixable issues, each enriched with a 'handler' function.
|
|
188
246
|
"""
|
|
189
247
|
fixable = []
|
|
190
248
|
for issue in issues:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
249
|
+
handler = self.registry.get_handler(issue.get("type", ""))
|
|
250
|
+
if handler:
|
|
251
|
+
# We use a temporary context in dry_run mode to check if handler can fix it
|
|
252
|
+
ctx = self._create_context(pathlib.Path(), issue, "")
|
|
253
|
+
result = handler(ctx)
|
|
254
|
+
if result["applied"]:
|
|
255
|
+
# Enriched issue with its handler and description
|
|
256
|
+
issue["handler"] = handler
|
|
257
|
+
issue["fix_description"] = result["message"]
|
|
194
258
|
fixable.append(issue)
|
|
195
|
-
break
|
|
196
259
|
return fixable
|
|
197
260
|
|
|
261
|
+
def _create_context(
|
|
262
|
+
self, file_path: pathlib.Path, issue: Dict[str, Any], content: str
|
|
263
|
+
) -> FixContext:
|
|
264
|
+
"""Creates a standardized FixContext."""
|
|
265
|
+
return {
|
|
266
|
+
"project_path": self.project_path,
|
|
267
|
+
"file_path": file_path,
|
|
268
|
+
"issue": issue,
|
|
269
|
+
"content": content,
|
|
270
|
+
"dry_run": self.dry_run,
|
|
271
|
+
}
|
|
272
|
+
|
|
198
273
|
def _check_git_status_with_prompt(self, interactive: bool) -> bool:
|
|
199
274
|
"""Checks git status and prompts user if needed. Returns True to continue."""
|
|
200
275
|
if self.dry_run:
|
|
@@ -224,56 +299,6 @@ class AutoFixer:
|
|
|
224
299
|
by_file[file_path].append(issue)
|
|
225
300
|
return by_file
|
|
226
301
|
|
|
227
|
-
def _apply_single_fix(
|
|
228
|
-
self,
|
|
229
|
-
file_path: pathlib.Path,
|
|
230
|
-
issue: Dict[str, Any],
|
|
231
|
-
original_content: str,
|
|
232
|
-
interactive: bool,
|
|
233
|
-
stats: Dict[str, int],
|
|
234
|
-
) -> bool:
|
|
235
|
-
"""Applies a single fix and updates stats. Returns True to continue, False to abort."""
|
|
236
|
-
fixer: FixStrategy = issue["fixer"]
|
|
237
|
-
description = fixer.get_description(issue)
|
|
238
|
-
|
|
239
|
-
print(f" Line {issue.get('line', '?')}: {description}")
|
|
240
|
-
|
|
241
|
-
if interactive and not self.dry_run:
|
|
242
|
-
# Show diff preview
|
|
243
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp:
|
|
244
|
-
tmp.write(original_content)
|
|
245
|
-
tmp_path = pathlib.Path(tmp.name)
|
|
246
|
-
|
|
247
|
-
try:
|
|
248
|
-
fixer.apply_fix(tmp_path, issue)
|
|
249
|
-
new_content = tmp_path.read_text(encoding="utf-8")
|
|
250
|
-
|
|
251
|
-
if new_content != original_content:
|
|
252
|
-
show_diff(file_path, original_content, new_content)
|
|
253
|
-
finally:
|
|
254
|
-
tmp_path.unlink()
|
|
255
|
-
|
|
256
|
-
response = input(" Apply fix? [y/n/q]: ").lower()
|
|
257
|
-
if response == "q":
|
|
258
|
-
print("Aborted by user.")
|
|
259
|
-
return False
|
|
260
|
-
if response != "y":
|
|
261
|
-
stats["skipped"] += 1
|
|
262
|
-
return True
|
|
263
|
-
|
|
264
|
-
if not self.dry_run:
|
|
265
|
-
success = fixer.apply_fix(file_path, issue)
|
|
266
|
-
if success:
|
|
267
|
-
stats["applied"] += 1
|
|
268
|
-
print(" ✅ Applied")
|
|
269
|
-
else:
|
|
270
|
-
stats["failed"] += 1
|
|
271
|
-
print(" ❌ Failed")
|
|
272
|
-
else:
|
|
273
|
-
stats["applied"] += 1 # Count as "would apply"
|
|
274
|
-
|
|
275
|
-
return True
|
|
276
|
-
|
|
277
302
|
def apply_fixes(self, issues: List[Dict[str, Any]], interactive: bool = True) -> Dict[str, int]:
|
|
278
303
|
"""Applies fixes to identified issues, grouping by file.
|
|
279
304
|
|
|
@@ -286,29 +311,80 @@ class AutoFixer:
|
|
|
286
311
|
"""
|
|
287
312
|
stats = {"applied": 0, "skipped": 0, "failed": 0}
|
|
288
313
|
|
|
289
|
-
# Git status check
|
|
290
314
|
if not self._check_git_status_with_prompt(interactive):
|
|
291
315
|
return stats
|
|
292
316
|
|
|
293
|
-
# Group by file
|
|
294
317
|
by_file = self._group_issues_by_file(issues)
|
|
295
318
|
|
|
296
319
|
for file_rel, file_issues in by_file.items():
|
|
297
320
|
file_path = self.project_path / file_rel
|
|
298
321
|
print(f"\n📄 {file_rel}")
|
|
299
322
|
|
|
300
|
-
# Read original content for diff
|
|
301
323
|
try:
|
|
302
|
-
|
|
324
|
+
current_content = file_path.read_text(encoding="utf-8")
|
|
303
325
|
except Exception as e:
|
|
304
326
|
print(f" ❌ Error reading file: {e}")
|
|
305
327
|
stats["failed"] += len(file_issues)
|
|
306
328
|
continue
|
|
307
329
|
|
|
330
|
+
file_modified = False
|
|
308
331
|
for issue in file_issues:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
332
|
+
handler = issue.get("handler")
|
|
333
|
+
if not handler:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
description = issue.get("fix_description", "Automatic fix")
|
|
337
|
+
print(f" Line {issue.get('line', '?')}: {description}")
|
|
338
|
+
|
|
339
|
+
# Context with current memory buffer
|
|
340
|
+
ctx = self._create_context(file_path, issue, current_content)
|
|
341
|
+
|
|
342
|
+
if interactive and not self.dry_run:
|
|
343
|
+
# In-memory transformation for preview
|
|
344
|
+
# For interactive mode, we show diff of the single fix
|
|
345
|
+
work_ctx = ctx.copy()
|
|
346
|
+
work_ctx["dry_run"] = False
|
|
347
|
+
result = handler(work_ctx)
|
|
348
|
+
|
|
349
|
+
if result["applied"] and result["new_content"]:
|
|
350
|
+
show_diff(file_path, current_content, result["new_content"])
|
|
351
|
+
else:
|
|
352
|
+
print(" (No changes suggested by handler)")
|
|
353
|
+
|
|
354
|
+
response = input(" Apply fix? [y/n/q]: ").lower()
|
|
355
|
+
if response == "q":
|
|
356
|
+
print("Aborted by user.")
|
|
357
|
+
return stats
|
|
358
|
+
if response != "y":
|
|
359
|
+
stats["skipped"] += 1
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
# Actual application on memory buffer
|
|
363
|
+
if not self.dry_run:
|
|
364
|
+
work_ctx = ctx.copy()
|
|
365
|
+
work_ctx["dry_run"] = False
|
|
366
|
+
result = handler(work_ctx)
|
|
367
|
+
|
|
368
|
+
if result["applied"] and result["new_content"]:
|
|
369
|
+
current_content = result["new_content"]
|
|
370
|
+
stats["applied"] += 1
|
|
371
|
+
file_modified = True
|
|
372
|
+
print(f" ✅ Applied: {result['message']}")
|
|
373
|
+
else:
|
|
374
|
+
stats["failed"] += 1
|
|
375
|
+
error = result.get("error", "Transformation returned no changes")
|
|
376
|
+
print(f" ❌ Failed: {error}")
|
|
377
|
+
else:
|
|
378
|
+
# Simulation
|
|
379
|
+
stats["applied"] += 1
|
|
380
|
+
|
|
381
|
+
# Write back the modified content once per file
|
|
382
|
+
if file_modified and not self.dry_run:
|
|
383
|
+
try:
|
|
384
|
+
file_path.write_text(current_content, encoding="utf-8")
|
|
385
|
+
except Exception as e:
|
|
386
|
+
print(f" ❌ Error writing back to file: {e}")
|
|
387
|
+
# We already counted them as applied, but technically it failed
|
|
388
|
+
pass
|
|
313
389
|
|
|
314
390
|
return stats
|
|
@@ -146,6 +146,45 @@ def _build_markdown_repo_standards(analyses: Dict[str, Any]) -> List[str]:
|
|
|
146
146
|
return lines
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def _build_markdown_research_metrics(research: Dict[str, Any]) -> List[str]:
|
|
150
|
+
"""Builds the research-based metrics section.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
research: The research summary dictionary.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A list of Markdown lines for the research metrics section.
|
|
157
|
+
"""
|
|
158
|
+
if not research:
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
lines = [
|
|
162
|
+
"\n## 🔬 Research-based Metrics",
|
|
163
|
+
f"- **Type Hint Coverage (Params)**: {research.get('type_hint_coverage')}% (Microsoft/Dropbox Std)",
|
|
164
|
+
f"- **Type Hint Coverage (Returns)**: {research.get('return_hint_coverage')}%",
|
|
165
|
+
f"- **Docstring Coverage**: {research.get('docstring_coverage')}% (PEP 257)",
|
|
166
|
+
]
|
|
167
|
+
styles = ", ".join(research.get("detected_docstring_styles", [])) or "PEP 257 (Default)"
|
|
168
|
+
lines.append(f"- **Detected Documentation Style**: {styles}")
|
|
169
|
+
return lines
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_markdown_general_metrics(metrics: Dict[str, Any]) -> List[str]:
|
|
173
|
+
"""Builds the general metrics section.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
metrics: The metrics dictionary.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
A list of Markdown lines for the general metrics section.
|
|
180
|
+
"""
|
|
181
|
+
lines = ["\n## 📊 General Metrics"]
|
|
182
|
+
for k, v in metrics.items():
|
|
183
|
+
if k not in ["quality_score", "maintainability_score", "overall_score"]:
|
|
184
|
+
lines.append(f"- **{k.replace('_', ' ').title()}**: {v}")
|
|
185
|
+
return lines
|
|
186
|
+
|
|
187
|
+
|
|
149
188
|
def _build_markdown_security_section(security: Dict[str, Any]) -> List[str]:
|
|
150
189
|
"""Builds the security analysis section with findings.
|
|
151
190
|
|
|
@@ -202,22 +241,16 @@ def generate_markdown_summary(analyses: Dict[str, Any], output_path: pathlib.Pat
|
|
|
202
241
|
)
|
|
203
242
|
f.write("\n")
|
|
204
243
|
|
|
244
|
+
# Research-based metrics
|
|
205
245
|
if research:
|
|
206
|
-
|
|
207
|
-
f.write(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
styles = ", ".join(research.get("detected_docstring_styles", [])) or "PEP 257 (Default)"
|
|
215
|
-
f.write(f"- **Detected Documentation Style**: {styles}\n")
|
|
216
|
-
|
|
217
|
-
f.write("\n## 📊 General Metrics\n")
|
|
218
|
-
for k, v in metrics.items():
|
|
219
|
-
if k not in ["quality_score", "maintainability_score", "overall_score"]:
|
|
220
|
-
f.write(f"- **{k.replace('_', ' ').title()}**: {v}\n")
|
|
246
|
+
research_lines = _build_markdown_research_metrics(research)
|
|
247
|
+
f.write("\n".join(research_lines))
|
|
248
|
+
f.write("\n")
|
|
249
|
+
|
|
250
|
+
# General metrics
|
|
251
|
+
general_metrics_lines = _build_markdown_general_metrics(metrics)
|
|
252
|
+
f.write("\n".join(general_metrics_lines))
|
|
253
|
+
f.write("\n")
|
|
221
254
|
|
|
222
255
|
# QGIS-specific findings
|
|
223
256
|
if project_type == "qgis":
|