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