1# Copyright 2019 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Error and warning message support for Emboss. 16 17This module exports the error, warn, and note functions, which return a _Message 18representing the error, warning, or note, respectively. The format method of 19the returned object can be used to render the message with source code snippets. 20 21Throughout Emboss, messages are passed around as lists of lists of _Messages. 22Each inner list represents a group of messages which should either all be 23printed, or not printed; i.e., an error message and associated informational 24messages. For example, to indicate both a duplicate definition error and a 25warning that a field is a reserved word, one might return: 26 27 return [ 28 [ 29 error.error(file_name, location, "Duplicate definition), 30 error.note(original_file_name, original_location, 31 "Original definition"), 32 ], 33 [ 34 error.warn(file_name, location, "Field name is a C reserved word.") 35 ], 36 ] 37""" 38 39from compiler.util import ir_data_utils 40from compiler.util import parser_types 41 42# Error levels; represented by the strings that will be included in messages. 43ERROR = "error" 44WARNING = "warning" 45NOTE = "note" 46 47# Colors; represented by the terminal escape sequences used to switch to them. 48# These work out-of-the-box on Unix derivatives (Linux, *BSD, Mac OS X), and 49# work on Windows using colorify. 50BLACK = "\033[0;30m" 51RED = "\033[0;31m" 52GREEN = "\033[0;32m" 53YELLOW = "\033[0;33m" 54BLUE = "\033[0;34m" 55MAGENTA = "\033[0;35m" 56CYAN = "\033[0;36m" 57WHITE = "\033[0;37m" 58BRIGHT_BLACK = "\033[0;1;30m" 59BRIGHT_RED = "\033[0;1;31m" 60BRIGHT_GREEN = "\033[0;1;32m" 61BRIGHT_YELLOW = "\033[0;1;33m" 62BRIGHT_BLUE = "\033[0;1;34m" 63BRIGHT_MAGENTA = "\033[0;1;35m" 64BRIGHT_CYAN = "\033[0;1;36m" 65BRIGHT_WHITE = "\033[0;1;37m" 66BOLD = "\033[0;1m" 67RESET = "\033[0m" 68 69def _copy(location): 70 location = ir_data_utils.copy(location) 71 if not location: 72 location = parser_types.make_location((0,0), (0,0)) 73 return location 74 75 76def error(source_file, location, message): 77 """Returns an object representing an error message.""" 78 return _Message(source_file, _copy(location), ERROR, message) 79 80 81def warn(source_file, location, message): 82 """Returns an object representing a warning.""" 83 return _Message(source_file, _copy(location), WARNING, message) 84 85 86def note(source_file, location, message): 87 """Returns and object representing an informational note.""" 88 return _Message(source_file, _copy(location), NOTE, message) 89 90 91class _Message(object): 92 """_Message holds a human-readable message.""" 93 __slots__ = ("location", "source_file", "severity", "message") 94 95 def __init__(self, source_file, location, severity, message): 96 self.location = location 97 self.source_file = source_file 98 self.severity = severity 99 self.message = message 100 101 def format(self, source_code): 102 """Formats the _Message for display. 103 104 Arguments: 105 source_code: A dict of file names to source texts. This is used to 106 render source snippets. 107 108 Returns: 109 A list of tuples. 110 111 The first element of each tuple is an escape sequence used to put a Unix 112 terminal into a particular color mode. For use in non-Unix-terminal 113 output, the string will match one of the color names exported by this 114 module. 115 116 The second element is a string containing text to show to the user. 117 118 The text will not end with a newline character, nor will it include a 119 RESET color element. 120 121 To show non-colorized output, simply write the second element of each 122 tuple, then a newline at the end. 123 124 To show colorized output, write both the first and second element of each 125 tuple, then a newline at the end. Before exiting to the operating system, 126 a RESET sequence should be emitted. 127 """ 128 # TODO(bolms): Figure out how to get Vim, Emacs, etc. to parse Emboss error 129 # messages. 130 severity_colors = { 131 ERROR: (BRIGHT_RED, BOLD), 132 WARNING: (BRIGHT_MAGENTA, BOLD), 133 NOTE: (BRIGHT_BLACK, WHITE) 134 } 135 136 result = [] 137 if self.location.is_synthetic: 138 pos = "[compiler bug]" 139 else: 140 pos = parser_types.format_position(self.location.start) 141 source_name = self.source_file or "[prelude]" 142 if not self.location.is_synthetic and self.source_file in source_code: 143 source_lines = source_code[self.source_file].splitlines() 144 source_line = source_lines[self.location.start.line - 1] 145 else: 146 source_line = "" 147 lines = self.message.splitlines() 148 for i in range(len(lines)): 149 line = lines[i] 150 # This is a little awkward, but we want to suppress the final newline in 151 # the message. This newline is final if and only if it is the last line 152 # of the message and there is no source snippet. 153 if i != len(lines) - 1 or source_line: 154 line += "\n" 155 result.append((BOLD, "{}:{}: ".format(source_name, pos))) 156 if i == 0: 157 severity = self.severity 158 else: 159 severity = NOTE 160 result.append((severity_colors[severity][0], "{}: ".format(severity))) 161 result.append((severity_colors[severity][1], line)) 162 if source_line: 163 result.append((WHITE, source_line + "\n")) 164 indicator_indent = " " * (self.location.start.column - 1) 165 if self.location.start.line == self.location.end.line: 166 indicator_caret = "^" * max( 167 1, self.location.end.column - self.location.start.column) 168 else: 169 indicator_caret = "^" 170 result.append((BRIGHT_GREEN, indicator_indent + indicator_caret)) 171 return result 172 173 def __repr__(self): 174 return ("Message({source_file!r}, make_location(({start_line!r}, " 175 "{start_column!r}), ({end_line!r}, {end_column!r}), " 176 "{is_synthetic!r}), {severity!r}, {message!r})").format( 177 source_file=self.source_file, 178 start_line=self.location.start.line, 179 start_column=self.location.start.column, 180 end_line=self.location.end.line, 181 end_column=self.location.end.column, 182 is_synthetic=self.location.is_synthetic, 183 severity=self.severity, 184 message=self.message) 185 186 def __eq__(self, other): 187 return ( 188 self.__class__ == other.__class__ and self.location == other.location 189 and self.source_file == other.source_file and 190 self.severity == other.severity and self.message == other.message) 191 192 def __ne__(self, other): 193 return not self == other 194 195 196def split_errors(errors): 197 """Splits errors into (user_errors, synthetic_errors). 198 199 Arguments: 200 errors: A list of lists of _Message, which is a list of bundles of 201 associated messages. 202 203 Returns: 204 (user_errors, synthetic_errors), where both user_errors and 205 synthetic_errors are lists of lists of _Message. synthetic_errors will 206 contain all bundles that reference any synthetic source_location, and 207 user_errors will contain the rest. 208 209 The intent is that user_errors can be shown to end users, while 210 synthetic_errors should generally be suppressed. 211 """ 212 synthetic_errors = [] 213 user_errors = [] 214 for error_block in errors: 215 if any(message.location.is_synthetic for message in error_block): 216 synthetic_errors.append(error_block) 217 else: 218 user_errors.append(error_block) 219 return user_errors, synthetic_errors 220 221 222def filter_errors(errors): 223 """Returns the non-synthetic errors from `errors`.""" 224 return split_errors(errors)[0] 225 226 227def format_errors(errors, source_codes, use_color=False): 228 """Formats error messages with source code snippets.""" 229 result = [] 230 for error_group in errors: 231 assert error_group, "Found empty error_group!" 232 for message in error_group: 233 if use_color: 234 result.append("".join(e[0] + e[1] + RESET 235 for e in message.format(source_codes))) 236 else: 237 result.append("".join(e[1] for e in message.format(source_codes))) 238 return "\n".join(result) 239 240 241def make_error_from_parse_error(file_name, parse_error): 242 return [error(file_name, 243 parse_error.token.source_location, 244 "{code}\n" 245 "Found {text!r} ({symbol}), expected {expected}.".format( 246 code=parse_error.code or "Syntax error", 247 text=parse_error.token.text, 248 symbol=parse_error.token.symbol, 249 expected=", ".join(parse_error.expected_tokens)))] 250 251 252