xref: /aosp_15_r20/external/emboss/compiler/util/error.py (revision 99e0aae7469b87d12f0ad23e61142c2d74c1ef70)
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