mirror of
https://github.com/Instagram/LibCST.git
synced 2025-12-23 10:35:53 +00:00
* Keep old exception messages (avoid breaking-changes for users relying on exception messages) * Move ``get_expected_str`` out of _exceptions.py, where it does not belong, to its own file in _parser/_parsing_check.py
191 lines
6.3 KiB
Python
191 lines
6.3 KiB
Python
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
#
|
|
# This source code is licensed under the MIT license found in the
|
|
# LICENSE file in the root directory of this source tree.
|
|
|
|
from enum import auto, Enum
|
|
from typing import Any, Callable, final, Optional, Sequence, Tuple
|
|
|
|
from libcst._tabs import expand_tabs
|
|
|
|
|
|
_NEWLINE_CHARS: str = "\r\n"
|
|
|
|
|
|
class EOFSentinel(Enum):
|
|
EOF = auto()
|
|
|
|
|
|
class CSTLogicError(Exception):
|
|
"""General purpose internal error within LibCST itself."""
|
|
|
|
pass
|
|
|
|
|
|
# pyre-fixme[2]: 'Any' type isn't pyre-strict.
|
|
def _parser_syntax_error_unpickle(kwargs: Any) -> "ParserSyntaxError":
|
|
return ParserSyntaxError(**kwargs)
|
|
|
|
|
|
@final
|
|
class PartialParserSyntaxError(Exception):
|
|
"""
|
|
An internal exception that represents a partially-constructed
|
|
:class:`ParserSyntaxError`. It's raised by our internal parser conversion functions,
|
|
which don't always know the current line and column information.
|
|
|
|
This partial object only contains a message, with the expectation that the line and
|
|
column information will be filled in by :class:`libcst._base_parser.BaseParser`.
|
|
|
|
This should never be visible to the end-user.
|
|
"""
|
|
|
|
message: str
|
|
|
|
def __init__(self, message: str) -> None:
|
|
self.message = message
|
|
|
|
|
|
@final
|
|
class ParserSyntaxError(Exception):
|
|
"""
|
|
Contains an error encountered while trying to parse a piece of source code. This
|
|
exception shouldn't be constructed directly by the user, but instead may be raised
|
|
by calls to :func:`parse_module`, :func:`parse_expression`, or
|
|
:func:`parse_statement`.
|
|
|
|
This does not inherit from :class:`SyntaxError` because Python's may raise a
|
|
:class:`SyntaxError` for any number of reasons, potentially leading to unintended
|
|
behavior.
|
|
"""
|
|
|
|
#: A human-readable explanation of the syntax error without information about where
|
|
#: the error occurred.
|
|
#:
|
|
#: For a human-readable explanation of the error alongside information about where
|
|
#: it occurred, use :meth:`__str__` (via ``str(ex)``) instead.
|
|
message: str
|
|
|
|
# An internal value used to compute `editor_column` and to pretty-print where the
|
|
# syntax error occurred in the code.
|
|
_lines: Sequence[str]
|
|
|
|
#: The one-indexed line where the error occured.
|
|
raw_line: int
|
|
|
|
#: The zero-indexed column as a number of characters from the start of the line
|
|
#: where the error occured.
|
|
raw_column: int
|
|
|
|
def __init__(
|
|
self, message: str, *, lines: Sequence[str], raw_line: int, raw_column: int
|
|
) -> None:
|
|
super(ParserSyntaxError, self).__init__(message)
|
|
self.message = message
|
|
self._lines = lines
|
|
self.raw_line = raw_line
|
|
self.raw_column = raw_column
|
|
|
|
def __reduce__(
|
|
self,
|
|
) -> Tuple[Callable[..., "ParserSyntaxError"], Tuple[object, ...]]:
|
|
return (
|
|
_parser_syntax_error_unpickle,
|
|
(
|
|
{
|
|
"message": self.message,
|
|
"lines": self._lines,
|
|
"raw_line": self.raw_line,
|
|
"raw_column": self.raw_column,
|
|
},
|
|
),
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
"""
|
|
A multi-line human-readable error message of where the syntax error is in their
|
|
code. For example::
|
|
|
|
Syntax Error @ 2:1.
|
|
Incomplete input. Encountered end of file (EOF), but expected 'except', or 'finally'.
|
|
|
|
try: pass
|
|
^
|
|
"""
|
|
context = self.context
|
|
return (
|
|
f"Syntax Error @ {self.editor_line}:{self.editor_column}.\n"
|
|
+ f"{self.message}"
|
|
+ (f"\n\n{context}" if context is not None else "")
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
"ParserSyntaxError("
|
|
+ f"{self.message!r}, lines=[...], raw_line={self.raw_line!r}, "
|
|
+ f"raw_column={self.raw_column!r})"
|
|
)
|
|
|
|
@property
|
|
def context(self) -> Optional[str]:
|
|
"""
|
|
A formatted string containing the line of code with the syntax error (or a
|
|
non-empty line above it) along with a caret indicating the exact column where
|
|
the error occurred.
|
|
|
|
Return ``None`` if there's no relevant non-empty line to show. (e.g. the file
|
|
consists of only blank lines)
|
|
"""
|
|
displayed_line = self.editor_line
|
|
displayed_column = self.editor_column
|
|
# we want to avoid displaying a blank line for context. If we're on a blank line
|
|
# find the nearest line above us that isn't blank.
|
|
while displayed_line >= 1 and not len(self._lines[displayed_line - 1].strip()):
|
|
displayed_line -= 1
|
|
displayed_column = len(self._lines[displayed_line - 1])
|
|
|
|
# only show context if we managed to find a non-empty line
|
|
if len(self._lines[displayed_line - 1].strip()):
|
|
formatted_source_line = expand_tabs(self._lines[displayed_line - 1]).rstrip(
|
|
_NEWLINE_CHARS
|
|
)
|
|
# fmt: off
|
|
return (
|
|
f"{formatted_source_line}\n"
|
|
+ f"{' ' * (displayed_column - 1)}^"
|
|
)
|
|
# fmt: on
|
|
else:
|
|
return None
|
|
|
|
@property
|
|
def editor_line(self) -> int:
|
|
"""
|
|
The expected one-indexed line in the user's editor. This is the same as
|
|
:attr:`raw_line`.
|
|
"""
|
|
return self.raw_line # raw_line is already one-indexed.
|
|
|
|
@property
|
|
def editor_column(self) -> int:
|
|
"""
|
|
The expected one-indexed column that's likely to match the behavior of the
|
|
user's editor, assuming tabs expand to 1-8 spaces. This is the column number
|
|
shown when the syntax error is printed out with `str`.
|
|
|
|
This assumes single-width characters. However, because python doesn't ship with
|
|
a wcwidth function, it's hard to handle this properly without a third-party
|
|
dependency.
|
|
|
|
For a raw zero-indexed character offset without tab expansion, see
|
|
:attr:`raw_column`.
|
|
"""
|
|
prefix_str = self._lines[self.raw_line - 1][: self.raw_column]
|
|
tab_adjusted_column = len(expand_tabs(prefix_str))
|
|
# Text editors use a one-indexed column, so we need to add one to our
|
|
# zero-indexed column to get a human-readable result.
|
|
return tab_adjusted_column + 1
|
|
|
|
|
|
class MetadataException(Exception):
|
|
pass
|