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

1"""Parser for mypy JSON output.""" 

2 

3from __future__ import annotations 

4 

5import json 

6from typing import Any 

7 

8from loguru import logger 

9 

10from lintro.parsers.base_parser import extract_int_field, extract_str_field 

11from lintro.parsers.mypy.mypy_issue import MypyIssue 

12 

13 

14def _parse_issue(item: dict[str, Any]) -> MypyIssue | None: 

15 """Convert a mypy JSON error object into a ``MypyIssue``. 

16 

17 Args: 

18 item: A single issue payload returned by mypy in JSON form. 

19 

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 

28 

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"]) 

33 

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 "" 

41 

42 message = str(item.get("message") or "").strip() 

43 severity = item.get("severity") 

44 

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 

58 

59 

60def _extract_errors(data: Any) -> list[dict[str, Any]]: 

61 """Extract error objects from parsed JSON data. 

62 

63 Args: 

64 data: The decoded JSON payload emitted by mypy. 

65 

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 [] 

83 

84 

85def parse_mypy_output(output: str) -> list[MypyIssue]: 

86 """Parse mypy JSON or JSON-lines output into ``MypyIssue`` objects. 

87 

88 Args: 

89 output: Raw stdout emitted by mypy using ``--output json``. 

90 

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 [] 

97 

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}") 

106 

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 

129 

130 return issues