Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/libcst/_exceptions.py: 55%
77 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:43 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:43 +0000
1# Copyright (c) Meta Platforms, Inc. and affiliates.
2#
3# This source code is licensed under the MIT license found in the
4# LICENSE file in the root directory of this source tree.
6from enum import auto, Enum
7from typing import Any, Callable, Iterable, Optional, Sequence, Tuple, Union
9from typing_extensions import final
11from libcst._parser.parso.pgen2.generator import ReservedString
12from libcst._parser.parso.python.token import PythonTokenTypes, TokenType
13from libcst._parser.types.token import Token
14from libcst._tabs import expand_tabs
16_EOF_STR: str = "end of file (EOF)"
17_INDENT_STR: str = "an indent"
18_DEDENT_STR: str = "a dedent"
19_NEWLINE_CHARS: str = "\r\n"
22class EOFSentinel(Enum):
23 EOF = auto()
26def get_expected_str(
27 encountered: Union[Token, EOFSentinel],
28 expected: Union[Iterable[Union[TokenType, ReservedString]], EOFSentinel],
29) -> str:
30 if (
31 isinstance(encountered, EOFSentinel)
32 or encountered.type is PythonTokenTypes.ENDMARKER
33 ):
34 encountered_str = _EOF_STR
35 elif encountered.type is PythonTokenTypes.INDENT:
36 encountered_str = _INDENT_STR
37 elif encountered.type is PythonTokenTypes.DEDENT:
38 encountered_str = _DEDENT_STR
39 else:
40 encountered_str = repr(encountered.string)
42 if isinstance(expected, EOFSentinel):
43 expected_names = [_EOF_STR]
44 else:
45 expected_names = sorted(
46 [
47 repr(el.name) if isinstance(el, TokenType) else repr(el.value)
48 for el in expected
49 ]
50 )
52 if len(expected_names) > 10:
53 # There's too many possibilities, so it's probably not useful to list them.
54 # Instead, let's just abbreviate the message.
55 return f"Unexpectedly encountered {encountered_str}."
56 else:
57 if len(expected_names) == 1:
58 expected_str = expected_names[0]
59 else:
60 expected_str = f"{', '.join(expected_names[:-1])}, or {expected_names[-1]}"
61 return f"Encountered {encountered_str}, but expected {expected_str}."
64# pyre-fixme[2]: 'Any' type isn't pyre-strict.
65def _parser_syntax_error_unpickle(kwargs: Any) -> "ParserSyntaxError":
66 return ParserSyntaxError(**kwargs)
69@final
70class PartialParserSyntaxError(Exception):
71 """
72 An internal exception that represents a partially-constructed
73 :class:`ParserSyntaxError`. It's raised by our internal parser conversion functions,
74 which don't always know the current line and column information.
76 This partial object only contains a message, with the expectation that the line and
77 column information will be filled in by :class:`libcst._base_parser.BaseParser`.
79 This should never be visible to the end-user.
80 """
82 message: str
84 def __init__(self, message: str) -> None:
85 self.message = message
88@final
89class ParserSyntaxError(Exception):
90 """
91 Contains an error encountered while trying to parse a piece of source code. This
92 exception shouldn't be constructed directly by the user, but instead may be raised
93 by calls to :func:`parse_module`, :func:`parse_expression`, or
94 :func:`parse_statement`.
96 This does not inherit from :class:`SyntaxError` because Python's may raise a
97 :class:`SyntaxError` for any number of reasons, potentially leading to unintended
98 behavior.
99 """
101 #: A human-readable explanation of the syntax error without information about where
102 #: the error occurred.
103 #:
104 #: For a human-readable explanation of the error alongside information about where
105 #: it occurred, use :meth:`__str__` (via ``str(ex)``) instead.
106 message: str
108 # An internal value used to compute `editor_column` and to pretty-print where the
109 # syntax error occurred in the code.
110 _lines: Sequence[str]
112 #: The one-indexed line where the error occured.
113 raw_line: int
115 #: The zero-indexed column as a number of characters from the start of the line
116 #: where the error occured.
117 raw_column: int
119 def __init__(
120 self, message: str, *, lines: Sequence[str], raw_line: int, raw_column: int
121 ) -> None:
122 super(ParserSyntaxError, self).__init__(message)
123 self.message = message
124 self._lines = lines
125 self.raw_line = raw_line
126 self.raw_column = raw_column
128 def __reduce__(
129 self,
130 ) -> Tuple[Callable[..., "ParserSyntaxError"], Tuple[object, ...]]:
131 return (
132 _parser_syntax_error_unpickle,
133 (
134 {
135 "message": self.message,
136 "lines": self._lines,
137 "raw_line": self.raw_line,
138 "raw_column": self.raw_column,
139 },
140 ),
141 )
143 def __str__(self) -> str:
144 """
145 A multi-line human-readable error message of where the syntax error is in their
146 code. For example::
148 Syntax Error @ 2:1.
149 Incomplete input. Encountered end of file (EOF), but expected 'except', or 'finally'.
151 try: pass
152 ^
153 """
154 context = self.context
155 return (
156 f"Syntax Error @ {self.editor_line}:{self.editor_column}.\n"
157 + f"{self.message}"
158 + (f"\n\n{context}" if context is not None else "")
159 )
161 def __repr__(self) -> str:
162 return (
163 "ParserSyntaxError("
164 + f"{self.message!r}, lines=[...], raw_line={self.raw_line!r}, "
165 + f"raw_column={self.raw_column!r})"
166 )
168 @property
169 def context(self) -> Optional[str]:
170 """
171 A formatted string containing the line of code with the syntax error (or a
172 non-empty line above it) along with a caret indicating the exact column where
173 the error occurred.
175 Return ``None`` if there's no relevant non-empty line to show. (e.g. the file
176 consists of only blank lines)
177 """
178 displayed_line = self.editor_line
179 displayed_column = self.editor_column
180 # we want to avoid displaying a blank line for context. If we're on a blank line
181 # find the nearest line above us that isn't blank.
182 while displayed_line >= 1 and not len(self._lines[displayed_line - 1].strip()):
183 displayed_line -= 1
184 displayed_column = len(self._lines[displayed_line - 1])
186 # only show context if we managed to find a non-empty line
187 if len(self._lines[displayed_line - 1].strip()):
188 formatted_source_line = expand_tabs(self._lines[displayed_line - 1]).rstrip(
189 _NEWLINE_CHARS
190 )
191 # fmt: off
192 return (
193 f"{formatted_source_line}\n"
194 + f"{' ' * (displayed_column - 1)}^"
195 )
196 # fmt: on
197 else:
198 return None
200 @property
201 def editor_line(self) -> int:
202 """
203 The expected one-indexed line in the user's editor. This is the same as
204 :attr:`raw_line`.
205 """
206 return self.raw_line # raw_line is already one-indexed.
208 @property
209 def editor_column(self) -> int:
210 """
211 The expected one-indexed column that's likely to match the behavior of the
212 user's editor, assuming tabs expand to 1-8 spaces. This is the column number
213 shown when the syntax error is printed out with `str`.
215 This assumes single-width characters. However, because python doesn't ship with
216 a wcwidth function, it's hard to handle this properly without a third-party
217 dependency.
219 For a raw zero-indexed character offset without tab expansion, see
220 :attr:`raw_column`.
221 """
222 prefix_str = self._lines[self.raw_line - 1][: self.raw_column]
223 tab_adjusted_column = len(expand_tabs(prefix_str))
224 # Text editors use a one-indexed column, so we need to add one to our
225 # zero-indexed column to get a human-readable result.
226 return tab_adjusted_column + 1
229class MetadataException(Exception):
230 pass