blob: c85d8fc09da514aa19486d4f7001512590a4fbda [file] [log] [blame]
Ben Olmsteadc0d77842019-07-31 17:34:05 -07001# 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
reventlov6731fc42019-10-03 15:23:13 -070039from compiler.util import parser_types
Ben Olmsteadc0d77842019-07-31 17:34:05 -070040
41# Error levels; represented by the strings that will be included in messages.
42ERROR = "error"
43WARNING = "warning"
44NOTE = "note"
45
46# Colors; represented by the terminal escape sequences used to switch to them.
47# These work out-of-the-box on Unix derivatives (Linux, *BSD, Mac OS X), and
48# work on Windows using colorify.
49BLACK = "\033[0;30m"
50RED = "\033[0;31m"
51GREEN = "\033[0;32m"
52YELLOW = "\033[0;33m"
53BLUE = "\033[0;34m"
54MAGENTA = "\033[0;35m"
55CYAN = "\033[0;36m"
56WHITE = "\033[0;37m"
57BRIGHT_BLACK = "\033[0;1;30m"
58BRIGHT_RED = "\033[0;1;31m"
59BRIGHT_GREEN = "\033[0;1;32m"
60BRIGHT_YELLOW = "\033[0;1;33m"
61BRIGHT_BLUE = "\033[0;1;34m"
62BRIGHT_MAGENTA = "\033[0;1;35m"
63BRIGHT_CYAN = "\033[0;1;36m"
64BRIGHT_WHITE = "\033[0;1;37m"
65BOLD = "\033[0;1m"
66RESET = "\033[0m"
67
68
69def error(source_file, location, message):
70 """Returns an object representing an error message."""
71 return _Message(source_file, location, ERROR, message)
72
73
74def warn(source_file, location, message):
75 """Returns an object representing a warning."""
76 return _Message(source_file, location, WARNING, message)
77
78
79def note(source_file, location, message):
80 """Returns and object representing an informational note."""
81 return _Message(source_file, location, NOTE, message)
82
83
84class _Message(object):
85 """_Message holds a human-readable message."""
86 __slots__ = ("location", "source_file", "severity", "message")
87
88 def __init__(self, source_file, location, severity, message):
89 self.location = location
90 self.source_file = source_file
91 self.severity = severity
92 self.message = message
93
94 def format(self, source_code):
95 """Formats the _Message for display.
96
97 Arguments:
98 source_code: A dict of file names to source texts. This is used to
99 render source snippets.
100
101 Returns:
102 A list of tuples.
103
104 The first element of each tuple is an escape sequence used to put a Unix
105 terminal into a particular color mode. For use in non-Unix-terminal
106 output, the string will match one of the color names exported by this
107 module.
108
109 The second element is a string containing text to show to the user.
110
111 The text will not end with a newline character, nor will it include a
112 RESET color element.
113
114 To show non-colorized output, simply write the second element of each
115 tuple, then a newline at the end.
116
117 To show colorized output, write both the first and second element of each
118 tuple, then a newline at the end. Before exiting to the operating system,
119 a RESET sequence should be emitted.
120 """
121 # TODO(bolms): Figure out how to get Vim, Emacs, etc. to parse Emboss error
122 # messages.
123 severity_colors = {
124 ERROR: (BRIGHT_RED, BOLD),
125 WARNING: (BRIGHT_MAGENTA, BOLD),
126 NOTE: (BRIGHT_BLACK, WHITE)
127 }
128
129 result = []
130 if self.location.is_synthetic:
131 pos = "[compiler bug]"
132 else:
133 pos = parser_types.format_position(self.location.start)
134 source_name = self.source_file or "[prelude]"
135 if not self.location.is_synthetic and self.source_file in source_code:
136 source_lines = source_code[self.source_file].splitlines()
137 source_line = source_lines[self.location.start.line - 1]
138 else:
139 source_line = ""
140 lines = self.message.splitlines()
141 for i in range(len(lines)):
142 line = lines[i]
143 # This is a little awkward, but we want to suppress the final newline in
144 # the message. This newline is final if and only if it is the last line
145 # of the message and there is no source snippet.
146 if i != len(lines) - 1 or source_line:
147 line += "\n"
148 result.append((BOLD, "{}:{}: ".format(source_name, pos)))
149 if i == 0:
150 severity = self.severity
151 else:
152 severity = NOTE
153 result.append((severity_colors[severity][0], "{}: ".format(severity)))
154 result.append((severity_colors[severity][1], line))
155 if source_line:
156 result.append((WHITE, source_line + "\n"))
157 indicator_indent = " " * (self.location.start.column - 1)
158 if self.location.start.line == self.location.end.line:
159 indicator_caret = "^" * max(
160 1, self.location.end.column - self.location.start.column)
161 else:
162 indicator_caret = "^"
163 result.append((BRIGHT_GREEN, indicator_indent + indicator_caret))
164 return result
165
166 def __repr__(self):
167 return ("Message({source_file!r}, make_location(({start_line!r}, "
168 "{start_column!r}), ({end_line!r}, {end_column!r}), "
169 "{is_synthetic!r}), {severity!r}, {message!r})").format(
170 source_file=self.source_file,
171 start_line=self.location.start.line,
172 start_column=self.location.start.column,
173 end_line=self.location.end.line,
174 end_column=self.location.end.column,
175 is_synthetic=self.location.is_synthetic,
176 severity=self.severity,
177 message=self.message)
178
179 def __eq__(self, other):
180 return (
181 self.__class__ == other.__class__ and self.location == other.location
182 and self.source_file == other.source_file and
183 self.severity == other.severity and self.message == other.message)
184
185 def __ne__(self, other):
186 return not self == other
187
188
189def split_errors(errors):
190 """Splits errors into (user_errors, synthetic_errors).
191
192 Arguments:
193 errors: A list of lists of _Message, which is a list of bundles of
194 associated messages.
195
196 Returns:
197 (user_errors, synthetic_errors), where both user_errors and
198 synthetic_errors are lists of lists of _Message. synthetic_errors will
199 contain all bundles that reference any synthetic source_location, and
200 user_errors will contain the rest.
201
202 The intent is that user_errors can be shown to end users, while
203 synthetic_errors should generally be suppressed.
204 """
205 synthetic_errors = []
206 user_errors = []
207 for error_block in errors:
208 if any(message.location.is_synthetic for message in error_block):
209 synthetic_errors.append(error_block)
210 else:
211 user_errors.append(error_block)
212 return user_errors, synthetic_errors
213
214
215def filter_errors(errors):
216 """Returns the non-synthetic errors from `errors`."""
217 return split_errors(errors)[0]
218
219
220def format_errors(errors, source_codes, use_color=False):
221 """Formats error messages with source code snippets."""
222 result = []
223 for error_group in errors:
224 assert error_group, "Found empty error_group!"
225 for message in error_group:
226 if use_color:
227 result.append("".join(e[0] + e[1] + RESET
228 for e in message.format(source_codes)))
229 else:
230 result.append("".join(e[1] for e in message.format(source_codes)))
231 return "\n".join(result)
232
233
234def make_error_from_parse_error(file_name, parse_error):
235 return [error(file_name,
236 parse_error.token.source_location,
237 "{code}\n"
238 "Found {text!r} ({symbol}), expected {expected}.".format(
239 code=parse_error.code or "Syntax error",
240 text=parse_error.token.text,
241 symbol=parse_error.token.symbol,
242 expected=", ".join(parse_error.expected_tokens)))]
243
244