improve Shapes::idx / Shapes::idx lint by underlining invalid property
[hiphop-php.git] / hphp / hack / test / parse_errors.py
blob5358617390f2a71d531ef796f8a3691a4c52487d
1 #!/usr/bin/env python3
2 # pyre-strict
4 import re
5 from dataclasses import dataclass
6 from typing import IO, List, Tuple
9 @dataclass
10 class ErrorCode:
11 type: str
12 code: int
15 @dataclass
16 class Position:
17 fileName: str
18 line: int
19 startColumn: int
20 endColumn: int
23 @dataclass
24 class PositionedMessage:
25 position: Position
26 message: str
29 @dataclass
30 class Error:
31 code: ErrorCode
32 message: PositionedMessage
33 reason: List[PositionedMessage]
36 class ParseException(Exception):
37 pass
40 def make_error(
41 errorCode: ErrorCode,
42 position: Position,
43 message: str,
44 reasons: List[PositionedMessage],
45 ) -> Error:
46 return Error(errorCode, PositionedMessage(position, message), reasons)
49 def end_of_file(line: str) -> bool:
50 return line == "" or line == "\n"
53 def parse_errors(output_file_name: str) -> List[Error]:
54 with open(output_file_name) as output_file:
55 try:
56 return parse_error(output_file, True)
57 except ParseException:
58 try:
59 return parse_error(output_file, False)
60 except ParseException as ex:
61 raise ParseException(f"at file {output_file_name}: {ex}")
64 def same_error(line: str, multiple_error_file: bool) -> bool:
65 if multiple_error_file:
66 return starts_with_space(line)
67 else:
68 return not end_of_file(line)
71 def parse_error(output_file: IO[str], multiple_error_file: bool) -> List[Error]:
72 errors = []
73 line = output_file.readline()
74 while not end_of_file(line):
75 if line == "No errors\n":
76 return []
77 position = parse_position(line)
78 line = output_file.readline()
79 if starts_with_space(line):
80 # this is part of an hh_show, so ignore and continue
81 while starts_with_space(line):
82 line = output_file.readline()
83 continue
84 (message, errorCode) = parse_message_and_code(output_file, line)
85 line = output_file.readline()
86 reasons = []
87 while same_error(line, multiple_error_file):
88 reasonPos = parse_position(line)
89 line = output_file.readline()
90 (line, reasonMessage) = parse_message(output_file, line)
91 reason = PositionedMessage(reasonPos, reasonMessage)
92 reasons.append(reason)
93 errors.append(make_error(errorCode, position, message, reasons))
94 return errors
97 position_regex = r'^\s*File "(.*)", line (\d+), characters (\d+)-(\d+):(\[\d+\])?\n'
100 def parse_position(line: str) -> Position:
101 match = re.match(position_regex, line)
102 if match is None:
103 raise ParseException(f"Could not parse position line: {line}")
104 file = match.group(1)
105 lineNum = int(match.group(2))
106 startCol = int(match.group(3))
107 endCol = int(match.group(4))
108 return Position(file, lineNum, startCol, endCol)
111 def parse_message_and_code(file: IO[str], line: str) -> Tuple[str, ErrorCode]:
112 message_chunks = []
113 message_and_code_regex = r"^\s*(.*) \((.*)\[(\d+)\]\)\n"
114 match = re.match(message_and_code_regex, line)
115 while match is None:
116 match = re.match(r"^\s*(.*)\n", line)
117 if match is None:
118 raise ParseException(f"Could not parse message line: {line}")
119 message_line = match.group(1)
120 message_chunks.append(message_line)
121 line = file.readline()
122 match = re.match(message_and_code_regex, line)
123 assert match is not None, "should have raised exception above"
124 message_line = match.group(1)
125 type = match.group(2)
126 code = int(match.group(3))
127 message_chunks.append(message_line)
128 message = "".join(message_chunks)
129 return (message, ErrorCode(type, code))
132 def parse_message(file: IO[str], line: str) -> Tuple[str, str]:
133 message_chunks = []
134 message_regex = r"^\s*(.*)\n"
136 match = re.match(message_regex, line)
137 if match is None:
138 raise ParseException(f"Could not parse message line: {line}")
139 message_line = match.group(1)
140 message_chunks.append(message_line)
142 line = file.readline()
143 match = re.match(position_regex, line)
144 while match is None and not end_of_file(line):
145 match = re.match(message_regex, line)
146 if match is None:
147 raise ParseException(f"Could not parse message line: {line}")
148 message_line = match.group(1)
149 message_chunks.append(message_line)
151 line = file.readline()
152 match = re.match(position_regex, line)
153 message = "".join(message_chunks)
154 return (line, message)
157 def starts_with_space(s: str) -> bool:
158 return re.match(r"^\s", s) is not None
161 def sprint_error(error: Error) -> str:
162 file = error.message.position.fileName
163 line = error.message.position.line
164 startCol = error.message.position.startColumn
165 endCol = error.message.position.endColumn
166 message = error.message.message
167 type = error.code.type
168 code = error.code.code
169 out = [
170 f"\033[91m{file}:{line}:{startCol},{endCol}:\033[0m "
171 f"{message} ({type}[{code}])\n"
174 for reason in error.reason:
175 file = reason.position.fileName
176 line = reason.position.line
177 startCol = reason.position.startColumn
178 endCol = reason.position.endColumn
179 message = reason.message
180 out.append(f" \033[91m{file}:{line}:{startCol},{endCol}:\033[0m {message}\n")
181 return "".join(out)
184 def sprint_errors(errors: List[Error]) -> str:
185 if not errors:
186 return "No errors\n"
187 out = []
188 for error in errors:
189 out.append(sprint_error(error))
190 return "".join(out)