lucidscan 0.5.12__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.
- lucidscan/__init__.py +12 -0
- lucidscan/bootstrap/__init__.py +26 -0
- lucidscan/bootstrap/paths.py +160 -0
- lucidscan/bootstrap/platform.py +111 -0
- lucidscan/bootstrap/validation.py +76 -0
- lucidscan/bootstrap/versions.py +119 -0
- lucidscan/cli/__init__.py +50 -0
- lucidscan/cli/__main__.py +8 -0
- lucidscan/cli/arguments.py +405 -0
- lucidscan/cli/commands/__init__.py +64 -0
- lucidscan/cli/commands/autoconfigure.py +294 -0
- lucidscan/cli/commands/help.py +69 -0
- lucidscan/cli/commands/init.py +656 -0
- lucidscan/cli/commands/list_scanners.py +59 -0
- lucidscan/cli/commands/scan.py +307 -0
- lucidscan/cli/commands/serve.py +142 -0
- lucidscan/cli/commands/status.py +84 -0
- lucidscan/cli/commands/validate.py +105 -0
- lucidscan/cli/config_bridge.py +152 -0
- lucidscan/cli/exit_codes.py +17 -0
- lucidscan/cli/runner.py +284 -0
- lucidscan/config/__init__.py +29 -0
- lucidscan/config/ignore.py +178 -0
- lucidscan/config/loader.py +431 -0
- lucidscan/config/models.py +316 -0
- lucidscan/config/validation.py +645 -0
- lucidscan/core/__init__.py +3 -0
- lucidscan/core/domain_runner.py +463 -0
- lucidscan/core/git.py +174 -0
- lucidscan/core/logging.py +34 -0
- lucidscan/core/models.py +207 -0
- lucidscan/core/streaming.py +340 -0
- lucidscan/core/subprocess_runner.py +164 -0
- lucidscan/detection/__init__.py +21 -0
- lucidscan/detection/detector.py +154 -0
- lucidscan/detection/frameworks.py +270 -0
- lucidscan/detection/languages.py +328 -0
- lucidscan/detection/tools.py +229 -0
- lucidscan/generation/__init__.py +15 -0
- lucidscan/generation/config_generator.py +275 -0
- lucidscan/generation/package_installer.py +330 -0
- lucidscan/mcp/__init__.py +20 -0
- lucidscan/mcp/formatter.py +510 -0
- lucidscan/mcp/server.py +297 -0
- lucidscan/mcp/tools.py +1049 -0
- lucidscan/mcp/watcher.py +237 -0
- lucidscan/pipeline/__init__.py +17 -0
- lucidscan/pipeline/executor.py +187 -0
- lucidscan/pipeline/parallel.py +181 -0
- lucidscan/plugins/__init__.py +40 -0
- lucidscan/plugins/coverage/__init__.py +28 -0
- lucidscan/plugins/coverage/base.py +160 -0
- lucidscan/plugins/coverage/coverage_py.py +454 -0
- lucidscan/plugins/coverage/istanbul.py +411 -0
- lucidscan/plugins/discovery.py +107 -0
- lucidscan/plugins/enrichers/__init__.py +61 -0
- lucidscan/plugins/enrichers/base.py +63 -0
- lucidscan/plugins/linters/__init__.py +26 -0
- lucidscan/plugins/linters/base.py +125 -0
- lucidscan/plugins/linters/biome.py +448 -0
- lucidscan/plugins/linters/checkstyle.py +393 -0
- lucidscan/plugins/linters/eslint.py +368 -0
- lucidscan/plugins/linters/ruff.py +498 -0
- lucidscan/plugins/reporters/__init__.py +45 -0
- lucidscan/plugins/reporters/base.py +30 -0
- lucidscan/plugins/reporters/json_reporter.py +79 -0
- lucidscan/plugins/reporters/sarif_reporter.py +303 -0
- lucidscan/plugins/reporters/summary_reporter.py +61 -0
- lucidscan/plugins/reporters/table_reporter.py +81 -0
- lucidscan/plugins/scanners/__init__.py +57 -0
- lucidscan/plugins/scanners/base.py +60 -0
- lucidscan/plugins/scanners/checkov.py +484 -0
- lucidscan/plugins/scanners/opengrep.py +464 -0
- lucidscan/plugins/scanners/trivy.py +492 -0
- lucidscan/plugins/test_runners/__init__.py +27 -0
- lucidscan/plugins/test_runners/base.py +111 -0
- lucidscan/plugins/test_runners/jest.py +381 -0
- lucidscan/plugins/test_runners/karma.py +481 -0
- lucidscan/plugins/test_runners/playwright.py +434 -0
- lucidscan/plugins/test_runners/pytest.py +598 -0
- lucidscan/plugins/type_checkers/__init__.py +27 -0
- lucidscan/plugins/type_checkers/base.py +106 -0
- lucidscan/plugins/type_checkers/mypy.py +355 -0
- lucidscan/plugins/type_checkers/pyright.py +313 -0
- lucidscan/plugins/type_checkers/typescript.py +280 -0
- lucidscan-0.5.12.dist-info/METADATA +242 -0
- lucidscan-0.5.12.dist-info/RECORD +91 -0
- lucidscan-0.5.12.dist-info/WHEEL +5 -0
- lucidscan-0.5.12.dist-info/entry_points.txt +34 -0
- lucidscan-0.5.12.dist-info/licenses/LICENSE +201 -0
- lucidscan-0.5.12.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Base class for type checker plugins.
|
|
2
|
+
|
|
3
|
+
All type checker plugins inherit from TypeCheckerPlugin and implement the check() method.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from lucidscan.core.models import ScanContext, UnifiedIssue, ToolDomain
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TypeCheckResult:
|
|
18
|
+
"""Result statistics from type checking."""
|
|
19
|
+
|
|
20
|
+
errors: int = 0
|
|
21
|
+
warnings: int = 0
|
|
22
|
+
notes: int = 0
|
|
23
|
+
files_checked: int = 0
|
|
24
|
+
details: List[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TypeCheckerPlugin(ABC):
|
|
28
|
+
"""Abstract base class for type checker plugins.
|
|
29
|
+
|
|
30
|
+
Type checker plugins provide static type checking functionality.
|
|
31
|
+
Each plugin wraps a specific type checking tool (mypy, pyright, tsc, etc.).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, project_root: Optional[Path] = None, **kwargs) -> None:
|
|
35
|
+
"""Initialize the type checker plugin.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
project_root: Optional project root for tool installation.
|
|
39
|
+
**kwargs: Additional arguments for subclasses.
|
|
40
|
+
"""
|
|
41
|
+
self._project_root = project_root
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def name(self) -> str:
|
|
46
|
+
"""Unique plugin identifier (e.g., 'mypy', 'pyright').
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Plugin name string.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def languages(self) -> List[str]:
|
|
55
|
+
"""Languages this type checker supports.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of language names (e.g., ['python'], ['typescript']).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def domain(self) -> ToolDomain:
|
|
63
|
+
"""Tool domain (always TYPE_CHECKING for type checkers).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
ToolDomain.TYPE_CHECKING
|
|
67
|
+
"""
|
|
68
|
+
return ToolDomain.TYPE_CHECKING
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def supports_strict_mode(self) -> bool:
|
|
72
|
+
"""Whether this type checker supports strict mode.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if the type checker has a strict mode.
|
|
76
|
+
"""
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def get_version(self) -> str:
|
|
81
|
+
"""Get the version of the underlying type checking tool.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Version string.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def ensure_binary(self) -> Path:
|
|
89
|
+
"""Ensure the type checking tool is installed.
|
|
90
|
+
|
|
91
|
+
Downloads or installs the tool if not present.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Path to the tool binary.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def check(self, context: ScanContext) -> List[UnifiedIssue]:
|
|
99
|
+
"""Run type checking on the specified paths.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
context: Scan context with paths and configuration.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of UnifiedIssue objects for each type error.
|
|
106
|
+
"""
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""mypy type checker plugin.
|
|
2
|
+
|
|
3
|
+
mypy is a static type checker for Python.
|
|
4
|
+
https://mypy-lang.org/
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from lucidscan.core.logging import get_logger
|
|
17
|
+
from lucidscan.core.models import (
|
|
18
|
+
ScanContext,
|
|
19
|
+
Severity,
|
|
20
|
+
ToolDomain,
|
|
21
|
+
UnifiedIssue,
|
|
22
|
+
)
|
|
23
|
+
from lucidscan.core.subprocess_runner import run_with_streaming
|
|
24
|
+
from lucidscan.plugins.type_checkers.base import TypeCheckerPlugin
|
|
25
|
+
|
|
26
|
+
LOGGER = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _glob_to_regex(pattern: str) -> str:
|
|
30
|
+
"""Convert a gitignore-style glob pattern to a regex for mypy.
|
|
31
|
+
|
|
32
|
+
mypy's --exclude flag expects Python regex patterns, not glob patterns.
|
|
33
|
+
This function converts common glob patterns to equivalent regex.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
pattern: Gitignore-style glob pattern (e.g., '**/.venv/**', '*.pyc').
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Regex pattern suitable for mypy --exclude.
|
|
40
|
+
"""
|
|
41
|
+
import re
|
|
42
|
+
|
|
43
|
+
# Handle common directory patterns like **/.venv/** or .venv/
|
|
44
|
+
# Extract the core directory/file name and create a simple regex
|
|
45
|
+
if pattern.startswith("**/") and pattern.endswith("/**"):
|
|
46
|
+
# Pattern like **/.venv/** - match directory anywhere in path
|
|
47
|
+
core = pattern[3:-3] # Remove **/ and /**
|
|
48
|
+
# Escape regex special chars and create pattern
|
|
49
|
+
escaped = re.escape(core)
|
|
50
|
+
return f"(^|/){escaped}(/|$)"
|
|
51
|
+
|
|
52
|
+
if pattern.startswith("**/"):
|
|
53
|
+
# Pattern like **/foo - match at end of any path
|
|
54
|
+
core = pattern[3:]
|
|
55
|
+
escaped = re.escape(core)
|
|
56
|
+
return f"(^|/){escaped}$"
|
|
57
|
+
|
|
58
|
+
if pattern.endswith("/**"):
|
|
59
|
+
# Pattern like foo/** - match directory at start
|
|
60
|
+
core = pattern[:-3]
|
|
61
|
+
escaped = re.escape(core)
|
|
62
|
+
return f"^{escaped}(/|$)"
|
|
63
|
+
|
|
64
|
+
if pattern.endswith("/"):
|
|
65
|
+
# Directory pattern like .venv/
|
|
66
|
+
core = pattern[:-1]
|
|
67
|
+
escaped = re.escape(core)
|
|
68
|
+
return f"(^|/){escaped}(/|$)"
|
|
69
|
+
|
|
70
|
+
# Handle wildcard patterns
|
|
71
|
+
# Escape all regex special chars first
|
|
72
|
+
escaped = re.escape(pattern)
|
|
73
|
+
# Convert glob wildcards to regex
|
|
74
|
+
# ** matches any path components
|
|
75
|
+
escaped = escaped.replace(r"\*\*", ".*")
|
|
76
|
+
# * matches anything except /
|
|
77
|
+
escaped = escaped.replace(r"\*", "[^/]*")
|
|
78
|
+
# ? matches single char except /
|
|
79
|
+
escaped = escaped.replace(r"\?", "[^/]")
|
|
80
|
+
|
|
81
|
+
return escaped
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# mypy severity mapping
|
|
85
|
+
# mypy outputs: error, warning, note
|
|
86
|
+
SEVERITY_MAP = {
|
|
87
|
+
"error": Severity.HIGH,
|
|
88
|
+
"warning": Severity.MEDIUM,
|
|
89
|
+
"note": Severity.LOW,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MypyChecker(TypeCheckerPlugin):
|
|
94
|
+
"""mypy type checker plugin for Python code analysis."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
97
|
+
"""Initialize MypyChecker.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
project_root: Optional project root for finding mypy installation.
|
|
101
|
+
"""
|
|
102
|
+
self._project_root = project_root
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def name(self) -> str:
|
|
106
|
+
"""Plugin identifier."""
|
|
107
|
+
return "mypy"
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def languages(self) -> List[str]:
|
|
111
|
+
"""Supported languages."""
|
|
112
|
+
return ["python"]
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def supports_strict_mode(self) -> bool:
|
|
116
|
+
"""mypy supports strict mode."""
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
def get_version(self) -> str:
|
|
120
|
+
"""Get mypy version.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Version string or 'unknown' if unable to determine.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
binary = self.ensure_binary()
|
|
127
|
+
result = subprocess.run(
|
|
128
|
+
[str(binary), "--version"],
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
encoding="utf-8",
|
|
132
|
+
errors="replace",
|
|
133
|
+
timeout=30,
|
|
134
|
+
)
|
|
135
|
+
# Output is like "mypy 1.8.0 (compiled: yes)"
|
|
136
|
+
if result.returncode == 0:
|
|
137
|
+
parts = result.stdout.strip().split()
|
|
138
|
+
if len(parts) >= 2:
|
|
139
|
+
return parts[1]
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
return "unknown"
|
|
143
|
+
|
|
144
|
+
def ensure_binary(self) -> Path:
|
|
145
|
+
"""Ensure mypy is available.
|
|
146
|
+
|
|
147
|
+
Checks for mypy in:
|
|
148
|
+
1. Project's .venv/bin/mypy
|
|
149
|
+
2. System PATH
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Path to mypy binary.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
FileNotFoundError: If mypy is not installed.
|
|
156
|
+
"""
|
|
157
|
+
# Check project venv first
|
|
158
|
+
if self._project_root:
|
|
159
|
+
venv_mypy = self._project_root / ".venv" / "bin" / "mypy"
|
|
160
|
+
if venv_mypy.exists():
|
|
161
|
+
return venv_mypy
|
|
162
|
+
|
|
163
|
+
# Check system PATH
|
|
164
|
+
mypy_path = shutil.which("mypy")
|
|
165
|
+
if mypy_path:
|
|
166
|
+
return Path(mypy_path)
|
|
167
|
+
|
|
168
|
+
raise FileNotFoundError(
|
|
169
|
+
"mypy is not installed. Install it with: pip install mypy"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def check(self, context: ScanContext) -> List[UnifiedIssue]:
|
|
173
|
+
"""Run mypy type checking.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
context: Scan context with paths and configuration.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of type checking issues.
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
binary = self.ensure_binary()
|
|
183
|
+
except FileNotFoundError as e:
|
|
184
|
+
LOGGER.warning(str(e))
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
# Build command
|
|
188
|
+
cmd = [
|
|
189
|
+
str(binary),
|
|
190
|
+
"--output", "json",
|
|
191
|
+
"--no-error-summary",
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
# Check for strict mode in config
|
|
195
|
+
if context.config and hasattr(context.config, "type_checking"):
|
|
196
|
+
type_config = context.config.type_checking
|
|
197
|
+
if hasattr(type_config, "strict") and type_config.strict:
|
|
198
|
+
cmd.append("--strict")
|
|
199
|
+
|
|
200
|
+
# Check for mypy config file
|
|
201
|
+
mypy_ini = context.project_root / "mypy.ini"
|
|
202
|
+
setup_cfg = context.project_root / "setup.cfg"
|
|
203
|
+
pyproject = context.project_root / "pyproject.toml"
|
|
204
|
+
|
|
205
|
+
if mypy_ini.exists():
|
|
206
|
+
cmd.extend(["--config-file", str(mypy_ini)])
|
|
207
|
+
elif setup_cfg.exists():
|
|
208
|
+
cmd.extend(["--config-file", str(setup_cfg)])
|
|
209
|
+
elif pyproject.exists():
|
|
210
|
+
cmd.extend(["--config-file", str(pyproject)])
|
|
211
|
+
|
|
212
|
+
# Add paths to check
|
|
213
|
+
paths = [str(p) for p in context.paths] if context.paths else ["."]
|
|
214
|
+
cmd.extend(paths)
|
|
215
|
+
|
|
216
|
+
# Add exclude patterns (convert glob patterns to regex for mypy)
|
|
217
|
+
exclude_patterns = context.get_exclude_patterns()
|
|
218
|
+
for pattern in exclude_patterns:
|
|
219
|
+
regex_pattern = _glob_to_regex(pattern)
|
|
220
|
+
cmd.extend(["--exclude", regex_pattern])
|
|
221
|
+
|
|
222
|
+
LOGGER.debug(f"Running: {' '.join(cmd)}")
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
result = run_with_streaming(
|
|
226
|
+
cmd=cmd,
|
|
227
|
+
cwd=context.project_root,
|
|
228
|
+
tool_name="mypy",
|
|
229
|
+
stream_handler=context.stream_handler,
|
|
230
|
+
timeout=180,
|
|
231
|
+
)
|
|
232
|
+
except subprocess.TimeoutExpired:
|
|
233
|
+
LOGGER.warning("mypy timed out after 180 seconds")
|
|
234
|
+
return []
|
|
235
|
+
except Exception as e:
|
|
236
|
+
LOGGER.error(f"Failed to run mypy: {e}")
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
# Parse output
|
|
240
|
+
issues = self._parse_output(result.stdout, context.project_root)
|
|
241
|
+
|
|
242
|
+
LOGGER.info(f"mypy found {len(issues)} issues")
|
|
243
|
+
return issues
|
|
244
|
+
|
|
245
|
+
def _parse_output(self, output: str, project_root: Path) -> List[UnifiedIssue]:
|
|
246
|
+
"""Parse mypy JSON output.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
output: JSON output from mypy (one JSON object per line).
|
|
250
|
+
project_root: Project root directory.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of UnifiedIssue objects.
|
|
254
|
+
"""
|
|
255
|
+
if not output.strip():
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
issues = []
|
|
259
|
+
for line in output.strip().split("\n"):
|
|
260
|
+
if not line.strip():
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
error = json.loads(line)
|
|
265
|
+
issue = self._error_to_issue(error, project_root)
|
|
266
|
+
if issue:
|
|
267
|
+
issues.append(issue)
|
|
268
|
+
except json.JSONDecodeError:
|
|
269
|
+
# Skip non-JSON lines (e.g., summary messages)
|
|
270
|
+
LOGGER.debug(f"Skipping non-JSON line: {line}")
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
return issues
|
|
274
|
+
|
|
275
|
+
def _error_to_issue(
|
|
276
|
+
self,
|
|
277
|
+
error: Dict[str, Any],
|
|
278
|
+
project_root: Path,
|
|
279
|
+
) -> Optional[UnifiedIssue]:
|
|
280
|
+
"""Convert mypy error to UnifiedIssue.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
error: mypy error dict from JSON output.
|
|
284
|
+
project_root: Project root directory.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
UnifiedIssue or None.
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
severity_str = error.get("severity", "error")
|
|
291
|
+
message = error.get("message", "")
|
|
292
|
+
file = error.get("file", "")
|
|
293
|
+
line = error.get("line")
|
|
294
|
+
column = error.get("column")
|
|
295
|
+
code = error.get("code", "")
|
|
296
|
+
|
|
297
|
+
# Get severity
|
|
298
|
+
severity = SEVERITY_MAP.get(severity_str, Severity.MEDIUM)
|
|
299
|
+
|
|
300
|
+
# Build file path
|
|
301
|
+
file_path = Path(file)
|
|
302
|
+
if not file_path.is_absolute():
|
|
303
|
+
file_path = project_root / file_path
|
|
304
|
+
|
|
305
|
+
# Generate deterministic ID
|
|
306
|
+
issue_id = self._generate_issue_id(code, file, line, column, message)
|
|
307
|
+
|
|
308
|
+
# Build title
|
|
309
|
+
title = f"[{code}] {message}" if code else message
|
|
310
|
+
|
|
311
|
+
return UnifiedIssue(
|
|
312
|
+
id=issue_id,
|
|
313
|
+
domain=ToolDomain.TYPE_CHECKING,
|
|
314
|
+
source_tool="mypy",
|
|
315
|
+
severity=severity,
|
|
316
|
+
rule_id=code or "unknown",
|
|
317
|
+
title=title,
|
|
318
|
+
description=message,
|
|
319
|
+
documentation_url=f"https://mypy.readthedocs.io/en/stable/error_code_list.html#{code}" if code else None,
|
|
320
|
+
file_path=file_path,
|
|
321
|
+
line_start=line,
|
|
322
|
+
line_end=line,
|
|
323
|
+
column_start=column,
|
|
324
|
+
fixable=False,
|
|
325
|
+
metadata={
|
|
326
|
+
"severity_raw": severity_str,
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
LOGGER.warning(f"Failed to parse mypy error: {e}")
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def _generate_issue_id(
|
|
334
|
+
self,
|
|
335
|
+
code: str,
|
|
336
|
+
file: str,
|
|
337
|
+
line: Optional[int],
|
|
338
|
+
column: Optional[int],
|
|
339
|
+
message: str,
|
|
340
|
+
) -> str:
|
|
341
|
+
"""Generate deterministic issue ID.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
code: Error code.
|
|
345
|
+
file: File path.
|
|
346
|
+
line: Line number.
|
|
347
|
+
column: Column number.
|
|
348
|
+
message: Error message.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Unique issue ID.
|
|
352
|
+
"""
|
|
353
|
+
content = f"{code}:{file}:{line or 0}:{column or 0}:{message}"
|
|
354
|
+
hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
|
|
355
|
+
return f"mypy-{code}-{hash_val}" if code else f"mypy-{hash_val}"
|