forked from soul-catcher/mypy-gitlab-code-quality
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
121 lines (97 loc) · 3.29 KB
/
__init__.py
File metadata and controls
121 lines (97 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
from __future__ import annotations
import hashlib
import json
import re
from enum import Enum
from functools import reduce
from sys import stdin, stdout
from typing import TYPE_CHECKING, TypedDict
if TYPE_CHECKING:
from collections.abc import Iterable
class Severity(str, Enum):
major = "major"
info = "info"
unknown = "unknown"
class GitlabIssueLines(TypedDict):
begin: int
class GitlabIssueLocation(TypedDict):
path: str
lines: GitlabIssueLines
class GitlabIssue(TypedDict):
description: str
check_name: str | None
fingerprint: str
severity: Severity
location: GitlabIssueLocation
def parse_issue(line: str, fingerprints: set[str] | None = None) -> GitlabIssue | None:
if line.startswith("{"):
try:
match = json.loads(line)
except json.JSONDecodeError:
match = None
if hint := match.get("hint"): # attach hint to message
match["message"] += "\n" + hint
else:
match = re.fullmatch(
r"(?P<file>.+?)"
r":(?P<line>\d+)(?::\d+)?" # ignore column number if exists
r":\s(?P<severity>\w+)"
r":\s(?P<message>.+?)"
r"(?:\s\s\[(?P<code>.*)])?",
line,
)
if match is None:
return None
error_levels_table = {"error": Severity.major, "note": Severity.info}
path = match["file"]
line_number = int(match["line"])
error_level = match["severity"]
message = match["message"]
error_code = match["code"]
if fingerprints is None:
fingerprints = set()
def make_fingerprint(salt: str) -> str:
fingerprint_text = f"{salt}::{path}::{error_level}::{error_code}::{message}"
return hashlib.md5(
fingerprint_text.encode("utf-8"),
usedforsecurity=False,
).hexdigest()
fingerprint = make_fingerprint("")
while fingerprint in fingerprints:
fingerprint = make_fingerprint(fingerprint)
fingerprints.add(fingerprint)
return {
"description": message,
"check_name": error_code,
"fingerprint": fingerprint,
"severity": error_levels_table.get(error_level, Severity.unknown),
"location": {
"path": path,
"lines": {
"begin": line_number,
},
},
}
def append_or_extend(issues: list[GitlabIssue], new: GitlabIssue) -> list[GitlabIssue]:
"""
Extend previous issue with description of new one in case of "note" error level.
It is useful to extend error issues with note issues to prevent inconsistent view
of code quality widget. For more information see
https://github.com/soul-catcher/mypy-gitlab-code-quality/pull/3
"""
is_extend_previous = (
new["severity"] == Severity.info
and issues
and issues[-1]["location"] == new["location"]
)
if is_extend_previous:
issues[-1]["description"] += "\n" + new["description"]
else:
issues.append(new)
return issues
def generate_report(lines: Iterable[str]) -> list[GitlabIssue]:
fingerprints: set[str] = set()
issues = filter(None, (parse_issue(line, fingerprints) for line in lines))
return reduce(append_or_extend, issues, [])
def main() -> None:
json.dump(generate_report(map(str.rstrip, stdin)), stdout, indent="\t")