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

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, Iterable, Optional, Sequence, Tuple, Union 

8 

9from typing_extensions import final 

10 

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 

15 

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" 

20 

21 

22class EOFSentinel(Enum): 

23 EOF = auto() 

24 

25 

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) 

41 

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 ) 

51 

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}." 

62 

63 

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

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

66 return ParserSyntaxError(**kwargs) 

67 

68 

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. 

75 

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`. 

78 

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

80 """ 

81 

82 message: str 

83 

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

85 self.message = message 

86 

87 

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`. 

95 

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 """ 

100 

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 

107 

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] 

111 

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

113 raw_line: int 

114 

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 

118 

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 

127 

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 ) 

142 

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:: 

147 

148 Syntax Error @ 2:1. 

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

150 

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 ) 

160 

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 ) 

167 

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. 

174 

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]) 

185 

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 

199 

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. 

207 

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`. 

214 

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. 

218 

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 

227 

228 

229class MetadataException(Exception): 

230 pass