Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/libcst/_exceptions.py: 64%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

56 statements  

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. 

5 

6from enum import auto, Enum 

7from typing import Any, Callable, final, Optional, Sequence, Tuple 

8 

9from libcst._tabs import expand_tabs 

10 

11 

12_NEWLINE_CHARS: str = "\r\n" 

13 

14 

15class EOFSentinel(Enum): 

16 EOF = auto() 

17 

18 

19class CSTLogicError(Exception): 

20 """General purpose internal error within LibCST itself.""" 

21 

22 pass 

23 

24 

25# pyre-fixme[2]: 'Any' type isn't pyre-strict. 

26def _parser_syntax_error_unpickle(kwargs: Any) -> "ParserSyntaxError": 

27 return ParserSyntaxError(**kwargs) 

28 

29 

30@final 

31class PartialParserSyntaxError(Exception): 

32 """ 

33 An internal exception that represents a partially-constructed 

34 :class:`ParserSyntaxError`. It's raised by our internal parser conversion functions, 

35 which don't always know the current line and column information. 

36 

37 This partial object only contains a message, with the expectation that the line and 

38 column information will be filled in by :class:`libcst._base_parser.BaseParser`. 

39 

40 This should never be visible to the end-user. 

41 """ 

42 

43 message: str 

44 

45 def __init__(self, message: str) -> None: 

46 self.message = message 

47 

48 

49@final 

50class ParserSyntaxError(Exception): 

51 """ 

52 Contains an error encountered while trying to parse a piece of source code. This 

53 exception shouldn't be constructed directly by the user, but instead may be raised 

54 by calls to :func:`parse_module`, :func:`parse_expression`, or 

55 :func:`parse_statement`. 

56 

57 This does not inherit from :class:`SyntaxError` because Python's may raise a 

58 :class:`SyntaxError` for any number of reasons, potentially leading to unintended 

59 behavior. 

60 """ 

61 

62 #: A human-readable explanation of the syntax error without information about where 

63 #: the error occurred. 

64 #: 

65 #: For a human-readable explanation of the error alongside information about where 

66 #: it occurred, use :meth:`__str__` (via ``str(ex)``) instead. 

67 message: str 

68 

69 # An internal value used to compute `editor_column` and to pretty-print where the 

70 # syntax error occurred in the code. 

71 _lines: Sequence[str] 

72 

73 #: The one-indexed line where the error occured. 

74 raw_line: int 

75 

76 #: The zero-indexed column as a number of characters from the start of the line 

77 #: where the error occured. 

78 raw_column: int 

79 

80 def __init__( 

81 self, message: str, *, lines: Sequence[str], raw_line: int, raw_column: int 

82 ) -> None: 

83 super(ParserSyntaxError, self).__init__(message) 

84 self.message = message 

85 self._lines = lines 

86 self.raw_line = raw_line 

87 self.raw_column = raw_column 

88 

89 def __reduce__( 

90 self, 

91 ) -> Tuple[Callable[..., "ParserSyntaxError"], Tuple[object, ...]]: 

92 return ( 

93 _parser_syntax_error_unpickle, 

94 ( 

95 { 

96 "message": self.message, 

97 "lines": self._lines, 

98 "raw_line": self.raw_line, 

99 "raw_column": self.raw_column, 

100 }, 

101 ), 

102 ) 

103 

104 def __str__(self) -> str: 

105 """ 

106 A multi-line human-readable error message of where the syntax error is in their 

107 code. For example:: 

108 

109 Syntax Error @ 2:1. 

110 Incomplete input. Encountered end of file (EOF), but expected 'except', or 'finally'. 

111 

112 try: pass 

113 ^ 

114 """ 

115 context = self.context 

116 return ( 

117 f"Syntax Error @ {self.editor_line}:{self.editor_column}.\n" 

118 + f"{self.message}" 

119 + (f"\n\n{context}" if context is not None else "") 

120 ) 

121 

122 def __repr__(self) -> str: 

123 return ( 

124 "ParserSyntaxError(" 

125 + f"{self.message!r}, lines=[...], raw_line={self.raw_line!r}, " 

126 + f"raw_column={self.raw_column!r})" 

127 ) 

128 

129 @property 

130 def context(self) -> Optional[str]: 

131 """ 

132 A formatted string containing the line of code with the syntax error (or a 

133 non-empty line above it) along with a caret indicating the exact column where 

134 the error occurred. 

135 

136 Return ``None`` if there's no relevant non-empty line to show. (e.g. the file 

137 consists of only blank lines) 

138 """ 

139 displayed_line = self.editor_line 

140 displayed_column = self.editor_column 

141 # we want to avoid displaying a blank line for context. If we're on a blank line 

142 # find the nearest line above us that isn't blank. 

143 while displayed_line >= 1 and not len(self._lines[displayed_line - 1].strip()): 

144 displayed_line -= 1 

145 displayed_column = len(self._lines[displayed_line - 1]) 

146 

147 # only show context if we managed to find a non-empty line 

148 if len(self._lines[displayed_line - 1].strip()): 

149 formatted_source_line = expand_tabs(self._lines[displayed_line - 1]).rstrip( 

150 _NEWLINE_CHARS 

151 ) 

152 # fmt: off 

153 return ( 

154 f"{formatted_source_line}\n" 

155 + f"{' ' * (displayed_column - 1)}^" 

156 ) 

157 # fmt: on 

158 else: 

159 return None 

160 

161 @property 

162 def editor_line(self) -> int: 

163 """ 

164 The expected one-indexed line in the user's editor. This is the same as 

165 :attr:`raw_line`. 

166 """ 

167 return self.raw_line # raw_line is already one-indexed. 

168 

169 @property 

170 def editor_column(self) -> int: 

171 """ 

172 The expected one-indexed column that's likely to match the behavior of the 

173 user's editor, assuming tabs expand to 1-8 spaces. This is the column number 

174 shown when the syntax error is printed out with `str`. 

175 

176 This assumes single-width characters. However, because python doesn't ship with 

177 a wcwidth function, it's hard to handle this properly without a third-party 

178 dependency. 

179 

180 For a raw zero-indexed character offset without tab expansion, see 

181 :attr:`raw_column`. 

182 """ 

183 prefix_str = self._lines[self.raw_line - 1][: self.raw_column] 

184 tab_adjusted_column = len(expand_tabs(prefix_str)) 

185 # Text editors use a one-indexed column, so we need to add one to our 

186 # zero-indexed column to get a human-readable result. 

187 return tab_adjusted_column + 1 

188 

189 

190class MetadataException(Exception): 

191 pass