Coverage for lintro / parsers / mypy / mypy_parser.py: 79%
71 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Parser for mypy JSON output."""
3from __future__ import annotations
5import json
6from typing import Any
8from loguru import logger
10from lintro.parsers.base_parser import extract_int_field, extract_str_field
11from lintro.parsers.mypy.mypy_issue import MypyIssue
14def _parse_issue(item: dict[str, Any]) -> MypyIssue | None:
15 """Convert a mypy JSON error object into a ``MypyIssue``.
17 Args:
18 item: A single issue payload returned by mypy in JSON form.
20 Returns:
21 A populated ``MypyIssue`` instance or ``None`` when the payload cannot
22 be parsed.
23 """
24 try:
25 file_path = extract_str_field(item, ["path", "filename", "file"])
26 if not file_path:
27 return None
29 line = extract_int_field(item, ["line"], default=0) or 0
30 column = extract_int_field(item, ["column"], default=0) or 0
31 end_line_int = extract_int_field(item, ["endLine", "end_line"])
32 end_column_int = extract_int_field(item, ["endColumn", "end_column"])
34 raw_code = item.get("code")
35 code: str
36 if isinstance(raw_code, dict):
37 raw_val = raw_code.get("code") or raw_code.get("id") or raw_code.get("text")
38 code = str(raw_val) if raw_val is not None else ""
39 else:
40 code = str(raw_code) if raw_code is not None else ""
42 message = str(item.get("message") or "").strip()
43 severity = item.get("severity")
45 return MypyIssue(
46 file=file_path,
47 line=line,
48 column=column,
49 code=code,
50 message=message,
51 severity=str(severity) if severity is not None else None,
52 end_line=end_line_int,
53 end_column=end_column_int,
54 )
55 except (KeyError, TypeError, ValueError) as e:
56 logger.debug(f"Failed to parse mypy issue item: {e}")
57 return None
60def _extract_errors(data: Any) -> list[dict[str, Any]]:
61 """Extract error objects from parsed JSON data.
63 Args:
64 data: The decoded JSON payload emitted by mypy.
66 Returns:
67 A list of error dictionaries ready for issue parsing.
68 """
69 if isinstance(data, list):
70 return [item for item in data if isinstance(item, dict)]
71 if isinstance(data, dict):
72 errors = data.get("errors") or data.get("messages") or []
73 if isinstance(errors, dict):
74 errors = [errors]
75 extracted = [item for item in errors if isinstance(item, dict)]
76 if not extracted and any(
77 key in data for key in ("path", "filename", "file", "message")
78 ):
79 # Treat single error dict payload as one issue
80 return [data]
81 return extracted
82 return []
85def parse_mypy_output(output: str) -> list[MypyIssue]:
86 """Parse mypy JSON or JSON-lines output into ``MypyIssue`` objects.
88 Args:
89 output: Raw stdout emitted by mypy using ``--output json``.
91 Returns:
92 A list of ``MypyIssue`` instances parsed from the output. Returns an
93 empty list when no issues are present or the output cannot be decoded.
94 """
95 if not output or not output.strip():
96 return []
98 try:
99 data = json.loads(output)
100 items = _extract_errors(data)
101 return [issue for item in items if (issue := _parse_issue(item))]
102 except json.JSONDecodeError as e:
103 logger.debug(f"Failed to parse mypy JSON output: {e}")
104 except (TypeError, ValueError) as e:
105 logger.debug(f"Error processing mypy output: {e}")
107 # Fallback: try JSON Lines format
108 issues: list[MypyIssue] = []
109 for line in output.splitlines():
110 line = line.strip()
111 if not line or not line.startswith("{"):
112 continue
113 try:
114 data = json.loads(line)
115 except json.JSONDecodeError:
116 continue
117 except (TypeError, ValueError) as e:
118 logger.debug(f"Error parsing mypy JSON line: {e}")
119 continue
120 try:
121 items = _extract_errors(data) if isinstance(data, (dict, list)) else []
122 for item in items:
123 parsed = _parse_issue(item)
124 if parsed:
125 issues.append(parsed)
126 except (TypeError, ValueError, KeyError) as e:
127 logger.debug(f"Error extracting mypy errors: {e}")
128 continue
130 return issues