Updating submodules
[hiphop-php.git] / hphp / hack / test / parse_errors.py
blob65391dfad33edc6860646a8f437eb0313222a7d3
1 #!/usr/bin/env python3
2 # pyre-strict
4 import re
5 from dataclasses import dataclass
6 from enum import Enum
7 from typing import IO, List, Tuple
10 @dataclass
11 class ErrorCode:
12 type: str
13 code: int
16 @dataclass
17 class Position:
18 fileName: str
19 line: int
20 startColumn: int
21 endColumn: int
24 @dataclass
25 class PositionedMessage:
26 position: Position
27 message: str
30 class Severity(str, Enum):
31 ERROR = "ERROR"
32 WARNING = "WARN"
35 @dataclass
36 class Error:
37 severity: Severity
38 code: ErrorCode
39 message: PositionedMessage
40 reason: List[PositionedMessage]
43 class ParseException(Exception):
44 pass
47 def make_error(
48 severity: Severity,
49 errorCode: ErrorCode,
50 position: Position,
51 message: str,
52 reasons: List[PositionedMessage],
53 ) -> Error:
54 return Error(severity, errorCode, PositionedMessage(position, message), reasons)
57 def end_of_file(line: str) -> bool:
58 return line == "" or line == "\n"
61 def parse_errors(output_file_name: str) -> List[Error]:
62 with open(output_file_name) as output_file:
63 try:
64 return parse_error(output_file, True)
65 except ParseException:
66 try:
67 return parse_error(output_file, False)
68 except ParseException as ex:
69 raise ParseException(f"at file {output_file_name}: {ex}")
72 def same_error(line: str, multiple_error_file: bool) -> bool:
73 if multiple_error_file:
74 return starts_with_space(line)
75 else:
76 return not end_of_file(line)
79 # parse a "plain" formatted error. The expected format is emittted by
80 # Errors.to_string (OCaml) or Error::display_plain() (Rust).
81 def parse_error(output_file: IO[str], multiple_error_file: bool) -> List[Error]:
82 errors = []
83 line = output_file.readline()
84 while not end_of_file(line):
85 if line == "No errors\n":
86 return []
87 severity_string, position_string = line.split(": ", 1)
88 severity = Severity(severity_string)
89 position = parse_position(line)
90 line = output_file.readline()
91 if starts_with_space(line):
92 # this is part of an hh_show, so ignore and continue
93 while starts_with_space(line):
94 line = output_file.readline()
95 continue
96 (message, errorCode) = parse_message_and_code(output_file, line)
97 line = output_file.readline()
98 reasons = []
99 while same_error(line, multiple_error_file):
100 reasonPos = parse_position(line)
101 line = output_file.readline()
102 (line, reasonMessage) = parse_message(output_file, line)
103 reason = PositionedMessage(reasonPos, reasonMessage)
104 reasons.append(reason)
105 errors.append(make_error(severity, errorCode, position, message, reasons))
106 return errors
109 position_regex = r'^\s*File "(.*)", line (\d+), characters (\d+)-(\d+):(\[\d+\])?\n'
112 def parse_position(line: str) -> Position:
113 match = re.match(position_regex, line)
114 if match is None:
115 raise ParseException(f"Could not parse position line: {line}")
116 file = match.group(1)
117 lineNum = int(match.group(2))
118 startCol = int(match.group(3))
119 endCol = int(match.group(4))
120 return Position(file, lineNum, startCol, endCol)
123 def parse_message_and_code(file: IO[str], line: str) -> Tuple[str, ErrorCode]:
124 message_chunks = []
125 message_and_code_regex = r"^\s*(.*) \((.*)\[(\d+)\]\)\n"
126 match = re.match(message_and_code_regex, line)
127 while match is None:
128 match = re.match(r"^\s*(.*)\n", line)
129 if match is None:
130 raise ParseException(f"Could not parse message line: {line}")
131 message_line = match.group(1)
132 message_chunks.append(message_line)
133 line = file.readline()
134 match = re.match(message_and_code_regex, line)
135 assert match is not None, "should have raised exception above"
136 message_line = match.group(1)
137 type = match.group(2)
138 code = int(match.group(3))
139 message_chunks.append(message_line)
140 message = "".join(message_chunks)
141 return (message, ErrorCode(type, code))
144 def parse_message(file: IO[str], line: str) -> Tuple[str, str]:
145 message_chunks = []
146 message_regex = r"^\s*(.*)\n"
148 match = re.match(message_regex, line)
149 if match is None:
150 raise ParseException(f"Could not parse message line: {line}")
151 message_line = match.group(1)
152 message_chunks.append(message_line)
154 line = file.readline()
155 match = re.match(position_regex, line)
156 while match is None and not end_of_file(line):
157 match = re.match(message_regex, line)
158 if match is None:
159 raise ParseException(f"Could not parse message line: {line}")
160 message_line = match.group(1)
161 message_chunks.append(message_line)
163 line = file.readline()
164 match = re.match(position_regex, line)
165 message = "".join(message_chunks)
166 return (line, message)
169 def starts_with_space(s: str) -> bool:
170 return re.match(r"^\s", s) is not None
173 def sprint_error(error: Error) -> str:
174 file = error.message.position.fileName
175 line = error.message.position.line
176 startCol = error.message.position.startColumn
177 endCol = error.message.position.endColumn
178 message = error.message.message
179 type = error.code.type
180 code = error.code.code
181 out = [
182 f"\033[91m{file}:{line}:{startCol},{endCol}:\033[0m "
183 f"{message} ({type}[{code}])\n"
186 for reason in error.reason:
187 file = reason.position.fileName
188 line = reason.position.line
189 startCol = reason.position.startColumn
190 endCol = reason.position.endColumn
191 message = reason.message
192 out.append(f" \033[91m{file}:{line}:{startCol},{endCol}:\033[0m {message}\n")
193 return "".join(out)
196 def sprint_errors(errors: List[Error]) -> str:
197 if not errors:
198 return "No errors\n"
199 out = []
200 for error in errors:
201 out.append(sprint_error(error))
202 return "".join(out)