python-checkup 0.0.1__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.
- python_checkup/__init__.py +9 -0
- python_checkup/__main__.py +3 -0
- python_checkup/analysis_request.py +35 -0
- python_checkup/analyzer_catalog.py +100 -0
- python_checkup/analyzers/__init__.py +54 -0
- python_checkup/analyzers/bandit.py +158 -0
- python_checkup/analyzers/basedpyright.py +103 -0
- python_checkup/analyzers/cached.py +106 -0
- python_checkup/analyzers/dependency_vulns.py +298 -0
- python_checkup/analyzers/deptry.py +142 -0
- python_checkup/analyzers/detect_secrets.py +101 -0
- python_checkup/analyzers/mypy.py +217 -0
- python_checkup/analyzers/radon.py +150 -0
- python_checkup/analyzers/registry.py +69 -0
- python_checkup/analyzers/ruff.py +256 -0
- python_checkup/analyzers/typos.py +80 -0
- python_checkup/analyzers/vulture.py +151 -0
- python_checkup/cache.py +244 -0
- python_checkup/cli.py +763 -0
- python_checkup/config.py +87 -0
- python_checkup/dedup.py +119 -0
- python_checkup/dependencies/discovery.py +192 -0
- python_checkup/detection.py +298 -0
- python_checkup/diff.py +130 -0
- python_checkup/discovery.py +180 -0
- python_checkup/formatters/__init__.py +0 -0
- python_checkup/formatters/badge.py +38 -0
- python_checkup/formatters/json_fmt.py +22 -0
- python_checkup/formatters/terminal.py +396 -0
- python_checkup/mcp/__init__.py +3 -0
- python_checkup/mcp/installer.py +119 -0
- python_checkup/mcp/server.py +411 -0
- python_checkup/models.py +114 -0
- python_checkup/plan.py +109 -0
- python_checkup/progress.py +95 -0
- python_checkup/runner.py +438 -0
- python_checkup/scoring/__init__.py +0 -0
- python_checkup/scoring/engine.py +397 -0
- python_checkup/skills/SKILL.md +416 -0
- python_checkup/skills/__init__.py +0 -0
- python_checkup/skills/agents.py +98 -0
- python_checkup/skills/installer.py +248 -0
- python_checkup/skills/rule_db.py +806 -0
- python_checkup/web/__init__.py +0 -0
- python_checkup/web/server.py +285 -0
- python_checkup/web/static/__init__.py +0 -0
- python_checkup/web/static/index.html +959 -0
- python_checkup/web/template.py +26 -0
- python_checkup-0.0.1.dist-info/METADATA +250 -0
- python_checkup-0.0.1.dist-info/RECORD +53 -0
- python_checkup-0.0.1.dist-info/WHEEL +4 -0
- python_checkup-0.0.1.dist-info/entry_points.txt +14 -0
- python_checkup-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from python_checkup.analysis_request import AnalysisRequest
|
|
9
|
+
from python_checkup.models import Category, Diagnostic, Severity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TyposAnalyzer:
|
|
13
|
+
"""Source-code typo detection via typos."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def name(self) -> str:
|
|
17
|
+
return "typos"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def category(self) -> Category:
|
|
21
|
+
return Category.QUALITY
|
|
22
|
+
|
|
23
|
+
async def is_available(self) -> bool:
|
|
24
|
+
return shutil.which("typos") is not None
|
|
25
|
+
|
|
26
|
+
async def analyze(self, request: AnalysisRequest) -> list[Diagnostic]:
|
|
27
|
+
if not request.files:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
cmd = ["typos", "--format", "json", *[str(path) for path in request.files]]
|
|
31
|
+
timeout = request.config.timeout
|
|
32
|
+
proc = await asyncio.create_subprocess_exec(
|
|
33
|
+
*cmd,
|
|
34
|
+
stdout=asyncio.subprocess.PIPE,
|
|
35
|
+
stderr=asyncio.subprocess.PIPE,
|
|
36
|
+
)
|
|
37
|
+
stdout, _stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
38
|
+
|
|
39
|
+
# typos exits 0 when clean, 2 when typos found.
|
|
40
|
+
if proc.returncode not in (0, 2):
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
output = stdout.decode().strip()
|
|
44
|
+
if not output:
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
diagnostics: list[Diagnostic] = []
|
|
48
|
+
for line in output.splitlines():
|
|
49
|
+
try:
|
|
50
|
+
raw = json.loads(line)
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if not isinstance(raw, dict):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
path = Path(str(raw.get("path", "unknown")))
|
|
58
|
+
typo = str(raw.get("typo", ""))
|
|
59
|
+
corrections = raw.get("corrections", [])
|
|
60
|
+
fix: str | None = None
|
|
61
|
+
if isinstance(corrections, list) and corrections:
|
|
62
|
+
fix = (
|
|
63
|
+
f"Replace with: {', '.join(str(item) for item in corrections[:3])}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
diagnostics.append(
|
|
67
|
+
Diagnostic(
|
|
68
|
+
file_path=path,
|
|
69
|
+
line=int(raw.get("line_num", 0) or 0),
|
|
70
|
+
column=int(raw.get("byte_offset", 0) or 0),
|
|
71
|
+
severity=Severity.INFO,
|
|
72
|
+
rule_id="TYPOS",
|
|
73
|
+
tool="typos",
|
|
74
|
+
category=Category.QUALITY,
|
|
75
|
+
message=f"Possible typo: {typo}",
|
|
76
|
+
fix=fix,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return diagnostics
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from python_checkup.analysis_request import AnalysisRequest
|
|
9
|
+
from python_checkup.models import Category, Diagnostic, Severity
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("python_checkup")
|
|
12
|
+
|
|
13
|
+
# Vulture item types and their human-readable labels
|
|
14
|
+
ITEM_TYPE_LABELS: dict[str, str] = {
|
|
15
|
+
"attribute": "Unused attribute",
|
|
16
|
+
"class": "Unused class",
|
|
17
|
+
"function": "Unused function",
|
|
18
|
+
"import": "Unused import",
|
|
19
|
+
"method": "Unused method",
|
|
20
|
+
"property": "Unused property",
|
|
21
|
+
"variable": "Unused variable",
|
|
22
|
+
"unreachable_code": "Unreachable code",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Map item types to severity
|
|
26
|
+
ITEM_TYPE_SEVERITY: dict[str, Severity] = {
|
|
27
|
+
"unreachable_code": Severity.ERROR, # Definitely dead
|
|
28
|
+
"import": Severity.WARNING, # Likely dead
|
|
29
|
+
"function": Severity.WARNING, # Likely dead
|
|
30
|
+
"method": Severity.WARNING, # Likely dead
|
|
31
|
+
"class": Severity.WARNING, # Likely dead
|
|
32
|
+
"variable": Severity.INFO, # Might be dynamic access
|
|
33
|
+
"attribute": Severity.INFO, # Might be dynamic access
|
|
34
|
+
"property": Severity.INFO, # Might be dynamic access
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VultureAnalyzer:
|
|
39
|
+
"""Dead code detection via Vulture."""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def name(self) -> str:
|
|
43
|
+
return "vulture"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def category(self) -> Category:
|
|
47
|
+
return Category.DEAD_CODE
|
|
48
|
+
|
|
49
|
+
async def is_available(self) -> bool:
|
|
50
|
+
"""Check if vulture is importable."""
|
|
51
|
+
try:
|
|
52
|
+
import vulture # noqa: F401
|
|
53
|
+
|
|
54
|
+
return True
|
|
55
|
+
except ImportError:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
async def analyze(
|
|
59
|
+
self,
|
|
60
|
+
request: AnalysisRequest,
|
|
61
|
+
) -> list[Diagnostic]:
|
|
62
|
+
"""Run Vulture dead code detection.
|
|
63
|
+
|
|
64
|
+
IMPORTANT: Vulture accumulates state across scavenge() calls.
|
|
65
|
+
A fresh Vulture() instance MUST be created for each analysis run.
|
|
66
|
+
"""
|
|
67
|
+
files = request.files
|
|
68
|
+
config = request.config_dict()
|
|
69
|
+
if not files:
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
min_confidence: int = 80
|
|
73
|
+
raw_conf = config.get("min_confidence", 80)
|
|
74
|
+
if isinstance(raw_conf, int | float):
|
|
75
|
+
min_confidence = int(raw_conf)
|
|
76
|
+
|
|
77
|
+
# Run in thread pool -- Vulture's AST parsing is CPU-bound
|
|
78
|
+
loop = asyncio.get_running_loop()
|
|
79
|
+
return await loop.run_in_executor(
|
|
80
|
+
None, self._analyze_sync, files, min_confidence
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _analyze_sync(
|
|
84
|
+
self,
|
|
85
|
+
files: list[Path],
|
|
86
|
+
min_confidence: int,
|
|
87
|
+
) -> list[Diagnostic]:
|
|
88
|
+
"""Synchronous analysis -- runs in thread pool."""
|
|
89
|
+
from vulture.core import Vulture
|
|
90
|
+
|
|
91
|
+
# CRITICAL: Fresh instance every time.
|
|
92
|
+
v = Vulture()
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
v.scavenge([str(f) for f in files])
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error("Vulture scavenge failed: %s", e)
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
unused_code = v.get_unused_code(min_confidence=min_confidence)
|
|
101
|
+
|
|
102
|
+
diagnostics: list[Diagnostic] = []
|
|
103
|
+
for item in unused_code:
|
|
104
|
+
severity = ITEM_TYPE_SEVERITY.get(item.typ, Severity.INFO)
|
|
105
|
+
type_label = ITEM_TYPE_LABELS.get(item.typ, f"Unused {item.typ}")
|
|
106
|
+
|
|
107
|
+
diagnostics.append(
|
|
108
|
+
Diagnostic(
|
|
109
|
+
file_path=Path(item.filename),
|
|
110
|
+
line=item.first_lineno,
|
|
111
|
+
column=0,
|
|
112
|
+
severity=severity,
|
|
113
|
+
rule_id=f"V-{item.typ}",
|
|
114
|
+
tool="vulture",
|
|
115
|
+
category=Category.DEAD_CODE,
|
|
116
|
+
message=(
|
|
117
|
+
f"{type_label}: '{item.name}' ({item.confidence}% confidence)"
|
|
118
|
+
),
|
|
119
|
+
fix=_dead_code_fix(item),
|
|
120
|
+
end_line=item.last_lineno,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return diagnostics
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _dead_code_fix(item: Any) -> str:
|
|
128
|
+
match item.typ:
|
|
129
|
+
case "import":
|
|
130
|
+
return f"Remove unused import '{item.name}'"
|
|
131
|
+
case "function":
|
|
132
|
+
return (
|
|
133
|
+
f"Remove unused function '{item.name}' or add it to a "
|
|
134
|
+
"whitelist if it's used via dynamic dispatch"
|
|
135
|
+
)
|
|
136
|
+
case "method":
|
|
137
|
+
return (
|
|
138
|
+
f"Remove unused method '{item.name}' or mark it with "
|
|
139
|
+
"@typing.override if it implements an interface"
|
|
140
|
+
)
|
|
141
|
+
case "class":
|
|
142
|
+
return (
|
|
143
|
+
f"Remove unused class '{item.name}' or add it to __all__ "
|
|
144
|
+
"if it's part of the public API"
|
|
145
|
+
)
|
|
146
|
+
case "variable":
|
|
147
|
+
return f"Remove unused variable '{item.name}' or prefix with underscore"
|
|
148
|
+
case "unreachable_code":
|
|
149
|
+
return "Remove unreachable code after return/raise/continue/break"
|
|
150
|
+
case _:
|
|
151
|
+
return f"Remove unused {item.typ} '{item.name}'"
|
python_checkup/cache.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from python_checkup import __version__
|
|
10
|
+
from python_checkup.models import Category, Diagnostic, Severity
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("python_checkup")
|
|
13
|
+
|
|
14
|
+
# Include the package version so any python-checkup upgrade automatically
|
|
15
|
+
# invalidates the cache (analyzer logic may have changed).
|
|
16
|
+
CACHE_VERSION = f"v1-{__version__}"
|
|
17
|
+
# Maximum age for cache entries before cleanup (7 days)
|
|
18
|
+
CACHE_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
|
|
19
|
+
# Maximum total cache size before cleanup (100 MB)
|
|
20
|
+
CACHE_MAX_SIZE_BYTES = 100 * 1024 * 1024
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AnalysisCache:
|
|
24
|
+
"""Per-file, per-analyzer cache backed by the local filesystem.
|
|
25
|
+
|
|
26
|
+
Each file's diagnostics are cached independently, keyed on the
|
|
27
|
+
SHA-256 hash of the file's content. When a file changes, its hash
|
|
28
|
+
changes, so the old cache entry is never read again.
|
|
29
|
+
|
|
30
|
+
The cache directory defaults to .python-checkup-cache/ in the project root.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, project_root: Path, enabled: bool = True) -> None:
|
|
34
|
+
self.project_root = project_root
|
|
35
|
+
self.enabled = enabled
|
|
36
|
+
self.cache_dir = project_root / ".python-checkup-cache" / CACHE_VERSION
|
|
37
|
+
self._hits = 0
|
|
38
|
+
self._misses = 0
|
|
39
|
+
|
|
40
|
+
if self.enabled:
|
|
41
|
+
self._ensure_cache_dir()
|
|
42
|
+
|
|
43
|
+
def _ensure_cache_dir(self) -> None:
|
|
44
|
+
"""Create cache directory and .gitignore if they don't exist."""
|
|
45
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
gitignore = self.project_root / ".python-checkup-cache" / ".gitignore"
|
|
48
|
+
if not gitignore.exists():
|
|
49
|
+
gitignore.write_text("*\n")
|
|
50
|
+
|
|
51
|
+
def _file_hash(self, path: Path) -> str:
|
|
52
|
+
"""Compute truncated SHA-256 hash of file contents.
|
|
53
|
+
|
|
54
|
+
Returns first 16 hex chars (64 bits) of SHA-256.
|
|
55
|
+
Reads file in binary mode to avoid encoding issues.
|
|
56
|
+
"""
|
|
57
|
+
h = hashlib.sha256()
|
|
58
|
+
try:
|
|
59
|
+
with open(path, "rb") as f:
|
|
60
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
61
|
+
h.update(chunk)
|
|
62
|
+
except OSError:
|
|
63
|
+
return "0" * 16
|
|
64
|
+
return h.hexdigest()[:16]
|
|
65
|
+
|
|
66
|
+
def _cache_key(self, analyzer_name: str, file_path: Path) -> str:
|
|
67
|
+
"""Build cache key: {analyzer_name}:{content_hash}."""
|
|
68
|
+
content_hash = self._file_hash(file_path)
|
|
69
|
+
return f"{analyzer_name}:{content_hash}"
|
|
70
|
+
|
|
71
|
+
def _cache_path(self, cache_key: str) -> Path:
|
|
72
|
+
# Replace colons for Windows compatibility
|
|
73
|
+
safe_key = cache_key.replace(":", "_")
|
|
74
|
+
return self.cache_dir / f"{safe_key}.json"
|
|
75
|
+
|
|
76
|
+
def get(self, analyzer_name: str, file_path: Path) -> list[Diagnostic] | None:
|
|
77
|
+
"""Return cached diagnostics if file content hasn't changed.
|
|
78
|
+
|
|
79
|
+
Returns None on cache miss. Returns list[Diagnostic] on hit
|
|
80
|
+
(which may be an empty list, meaning "no issues found").
|
|
81
|
+
"""
|
|
82
|
+
if not self.enabled:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
cache_key = self._cache_key(analyzer_name, file_path)
|
|
86
|
+
cache_file = self._cache_path(cache_key)
|
|
87
|
+
|
|
88
|
+
if not cache_file.exists():
|
|
89
|
+
self._misses += 1
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
raw = json.loads(cache_file.read_text())
|
|
94
|
+
diagnostics = [_deserialize_diagnostic(d) for d in raw]
|
|
95
|
+
self._hits += 1
|
|
96
|
+
logger.debug("Cache hit: %s for %s", analyzer_name, file_path.name)
|
|
97
|
+
return diagnostics
|
|
98
|
+
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
|
|
99
|
+
logger.debug("Cache corrupted for %s, removing", cache_key)
|
|
100
|
+
cache_file.unlink(missing_ok=True)
|
|
101
|
+
self._misses += 1
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def set(
|
|
105
|
+
self,
|
|
106
|
+
analyzer_name: str,
|
|
107
|
+
file_path: Path,
|
|
108
|
+
diagnostics: list[Diagnostic],
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Cache diagnostics for a file.
|
|
111
|
+
|
|
112
|
+
Stores an empty list for clean files (so we know "no issues"
|
|
113
|
+
vs "not yet analyzed").
|
|
114
|
+
"""
|
|
115
|
+
if not self.enabled:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
cache_key = self._cache_key(analyzer_name, file_path)
|
|
119
|
+
cache_file = self._cache_path(cache_key)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
serialized = [_serialize_diagnostic(d) for d in diagnostics]
|
|
123
|
+
cache_file.write_text(json.dumps(serialized, default=str))
|
|
124
|
+
except OSError as e:
|
|
125
|
+
logger.debug("Failed to write cache for %s: %s", cache_key, e)
|
|
126
|
+
|
|
127
|
+
def get_stats(self) -> dict[str, int]:
|
|
128
|
+
total = self._hits + self._misses
|
|
129
|
+
return {
|
|
130
|
+
"hits": self._hits,
|
|
131
|
+
"misses": self._misses,
|
|
132
|
+
"total": total,
|
|
133
|
+
"hit_rate_pct": (round(self._hits / total * 100) if total > 0 else 0),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def clear(self) -> int:
|
|
137
|
+
"""Delete all cache entries. Returns number of entries deleted."""
|
|
138
|
+
count = 0
|
|
139
|
+
if self.cache_dir.exists():
|
|
140
|
+
for f in self.cache_dir.glob("*.json"):
|
|
141
|
+
f.unlink()
|
|
142
|
+
count += 1
|
|
143
|
+
logger.info("Cleared %d cache entries", count)
|
|
144
|
+
return count
|
|
145
|
+
|
|
146
|
+
def cleanup(self) -> int:
|
|
147
|
+
"""Remove stale cache entries older than CACHE_MAX_AGE_SECONDS.
|
|
148
|
+
|
|
149
|
+
Also enforces CACHE_MAX_SIZE_BYTES by removing oldest entries
|
|
150
|
+
first. Returns number of entries removed.
|
|
151
|
+
"""
|
|
152
|
+
if not self.cache_dir.exists():
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
now = time.time()
|
|
156
|
+
entries: list[tuple[Path, float, int]] = []
|
|
157
|
+
removed = 0
|
|
158
|
+
|
|
159
|
+
for f in self.cache_dir.glob("*.json"):
|
|
160
|
+
stat = f.stat()
|
|
161
|
+
age = now - stat.st_mtime
|
|
162
|
+
if age > CACHE_MAX_AGE_SECONDS:
|
|
163
|
+
f.unlink()
|
|
164
|
+
removed += 1
|
|
165
|
+
else:
|
|
166
|
+
entries.append((f, stat.st_mtime, stat.st_size))
|
|
167
|
+
|
|
168
|
+
# Enforce size limit: remove oldest first
|
|
169
|
+
total_size = sum(size for _, _, size in entries)
|
|
170
|
+
if total_size > CACHE_MAX_SIZE_BYTES:
|
|
171
|
+
entries.sort(key=lambda e: e[1]) # oldest first
|
|
172
|
+
for path, _, size in entries:
|
|
173
|
+
if total_size <= CACHE_MAX_SIZE_BYTES:
|
|
174
|
+
break
|
|
175
|
+
path.unlink()
|
|
176
|
+
total_size -= size
|
|
177
|
+
removed += 1
|
|
178
|
+
|
|
179
|
+
if removed > 0:
|
|
180
|
+
logger.debug("Cache cleanup: removed %d stale entries", removed)
|
|
181
|
+
return removed
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _serialize_diagnostic(d: Diagnostic) -> dict[str, object]:
|
|
185
|
+
return {
|
|
186
|
+
"file_path": str(d.file_path),
|
|
187
|
+
"line": d.line,
|
|
188
|
+
"column": d.column,
|
|
189
|
+
"severity": d.severity.value,
|
|
190
|
+
"rule_id": d.rule_id,
|
|
191
|
+
"tool": d.tool,
|
|
192
|
+
"category": d.category.value,
|
|
193
|
+
"message": d.message,
|
|
194
|
+
"fix": d.fix,
|
|
195
|
+
"help_url": d.help_url,
|
|
196
|
+
"end_line": d.end_line,
|
|
197
|
+
"end_column": d.end_column,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _deserialize_diagnostic(data: dict[str, object]) -> Diagnostic:
|
|
202
|
+
file_path = data["file_path"]
|
|
203
|
+
line = data["line"]
|
|
204
|
+
column = data["column"]
|
|
205
|
+
severity_val = data["severity"]
|
|
206
|
+
rule_id = data["rule_id"]
|
|
207
|
+
tool = data["tool"]
|
|
208
|
+
category_val = data["category"]
|
|
209
|
+
message = data["message"]
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
not isinstance(file_path, str)
|
|
213
|
+
or not isinstance(line, int)
|
|
214
|
+
or not isinstance(column, int)
|
|
215
|
+
or not isinstance(severity_val, str)
|
|
216
|
+
or not isinstance(rule_id, str)
|
|
217
|
+
or not isinstance(tool, str)
|
|
218
|
+
or not isinstance(category_val, str)
|
|
219
|
+
or not isinstance(message, str)
|
|
220
|
+
):
|
|
221
|
+
msg = "Invalid diagnostic data types"
|
|
222
|
+
raise TypeError(msg)
|
|
223
|
+
|
|
224
|
+
fix = data.get("fix")
|
|
225
|
+
help_url = data.get("help_url")
|
|
226
|
+
end_line = data.get("end_line")
|
|
227
|
+
end_column = data.get("end_column")
|
|
228
|
+
|
|
229
|
+
return Diagnostic(
|
|
230
|
+
file_path=Path(file_path),
|
|
231
|
+
line=line,
|
|
232
|
+
column=column,
|
|
233
|
+
severity=Severity(severity_val),
|
|
234
|
+
rule_id=rule_id,
|
|
235
|
+
tool=tool,
|
|
236
|
+
category=Category(category_val),
|
|
237
|
+
message=message,
|
|
238
|
+
fix=str(fix) if fix is not None else None,
|
|
239
|
+
help_url=str(help_url) if help_url is not None else None,
|
|
240
|
+
end_line=int(end_line) if isinstance(end_line, int | float | str) else None,
|
|
241
|
+
end_column=int(end_column)
|
|
242
|
+
if isinstance(end_column, int | float | str)
|
|
243
|
+
else None,
|
|
244
|
+
)
|