1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2011 - 2014 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the syntax check for Python 2/3. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 if sys.version_info[0] >= 3: |
|
12 if __name__ == '__main__': |
|
13 from py3flakes.checker import Checker |
|
14 from py3flakes.messages import ImportStarUsed |
|
15 else: |
|
16 from .py3flakes.checker import Checker #__IGNORE_WARNING__ |
|
17 from .py3flakes.messages import ImportStarUsed #__IGNORE_WARNING__ |
|
18 else: |
|
19 str = unicode #__IGNORE_WARNING__ |
|
20 if __name__ == '__main__': |
|
21 from py2flakes.checker import Checker #__IGNORE_WARNING__ |
|
22 from py2flakes.messages import ImportStarUsed #__IGNORE_WARNING__ |
|
23 else: |
|
24 from .py2flakes.checker import Checker #__IGNORE_WARNING__ |
|
25 from .py2flakes.messages import ImportStarUsed #__IGNORE_WARNING__ |
|
26 |
|
27 import re |
|
28 import traceback |
|
29 from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF32 |
|
30 |
|
31 try: |
|
32 import Preferences |
|
33 except (ImportError): |
|
34 pass |
|
35 |
|
36 codingBytes_regexps = [ |
|
37 (2, re.compile(br'''coding[:=]\s*([-\w_.]+)''')), |
|
38 (1, re.compile(br'''<\?xml.*\bencoding\s*=\s*['"]([-\w_.]+)['"]\?>''')), |
|
39 ] |
|
40 |
|
41 |
|
42 def get_codingBytes(text): |
|
43 """ |
|
44 Function to get the coding of a bytes text. |
|
45 |
|
46 @param text bytes text to inspect (bytes) |
|
47 @return coding string |
|
48 """ |
|
49 lines = text.splitlines() |
|
50 for coding in codingBytes_regexps: |
|
51 coding_re = coding[1] |
|
52 head = lines[:coding[0]] |
|
53 for l in head: |
|
54 m = coding_re.search(l) |
|
55 if m: |
|
56 return str(m.group(1), "ascii").lower() |
|
57 return None |
|
58 |
|
59 |
|
60 def decode(text): |
|
61 """ |
|
62 Function to decode some byte text into a string. |
|
63 |
|
64 @param text byte text to decode (bytes) |
|
65 @return tuple of decoded text and encoding (string, string) |
|
66 """ |
|
67 try: |
|
68 if text.startswith(BOM_UTF8): |
|
69 # UTF-8 with BOM |
|
70 return str(text[len(BOM_UTF8):], 'utf-8'), 'utf-8-bom' |
|
71 elif text.startswith(BOM_UTF16): |
|
72 # UTF-16 with BOM |
|
73 return str(text[len(BOM_UTF16):], 'utf-16'), 'utf-16' |
|
74 elif text.startswith(BOM_UTF32): |
|
75 # UTF-32 with BOM |
|
76 return str(text[len(BOM_UTF32):], 'utf-32'), 'utf-32' |
|
77 coding = get_codingBytes(text) |
|
78 if coding: |
|
79 return str(text, coding), coding |
|
80 except (UnicodeError, LookupError): |
|
81 pass |
|
82 |
|
83 # Assume UTF-8 |
|
84 try: |
|
85 return str(text, 'utf-8'), 'utf-8-guessed' |
|
86 except (UnicodeError, LookupError): |
|
87 pass |
|
88 |
|
89 try: |
|
90 guess = None |
|
91 if Preferences.getEditor("AdvancedEncodingDetection"): |
|
92 # Try the universal character encoding detector |
|
93 try: |
|
94 import ThirdParty.CharDet.chardet |
|
95 guess = ThirdParty.CharDet.chardet.detect(text) |
|
96 if guess and guess['confidence'] > 0.95 \ |
|
97 and guess['encoding'] is not None: |
|
98 codec = guess['encoding'].lower() |
|
99 return str(text, codec), '{0}-guessed'.format(codec) |
|
100 except (UnicodeError, LookupError, ImportError): |
|
101 pass |
|
102 except (NameError): |
|
103 pass |
|
104 |
|
105 # Try default encoding |
|
106 try: |
|
107 codec = Preferences.getEditor("DefaultEncoding") |
|
108 return str(text, codec), '{0}-default'.format(codec) |
|
109 except (UnicodeError, LookupError, NameError): |
|
110 pass |
|
111 |
|
112 try: |
|
113 if Preferences.getEditor("AdvancedEncodingDetection"): |
|
114 # Use the guessed one even if confifence level is low |
|
115 if guess and guess['encoding'] is not None: |
|
116 try: |
|
117 codec = guess['encoding'].lower() |
|
118 return str(text, codec), '{0}-guessed'.format(codec) |
|
119 except (UnicodeError, LookupError): |
|
120 pass |
|
121 except (NameError): |
|
122 pass |
|
123 |
|
124 # Assume UTF-8 loosing information |
|
125 return str(text, "utf-8", "ignore"), 'utf-8-ignore' |
|
126 |
|
127 |
|
128 def readEncodedFile(filename): |
|
129 """ |
|
130 Function to read a file and decode it's contents into proper text. |
|
131 |
|
132 @param filename name of the file to read (string) |
|
133 @return tuple of decoded text and encoding (string, string) |
|
134 """ |
|
135 try: |
|
136 filename = filename.encode(sys.getfilesystemencoding()) |
|
137 except (UnicodeDecodeError): |
|
138 pass |
|
139 f = open(filename, "rb") |
|
140 text = f.read() |
|
141 f.close() |
|
142 return decode(text) |
|
143 |
|
144 |
|
145 def normalizeCode(codestring): |
|
146 """ |
|
147 Function to normalize the given code. |
|
148 |
|
149 @param codestring code to be normalized (string) |
|
150 @return normalized code (string) |
|
151 """ |
|
152 codestring = codestring.replace("\r\n", "\n").replace("\r", "\n") |
|
153 |
|
154 if codestring and codestring[-1] != '\n': |
|
155 codestring = codestring + '\n' |
|
156 |
|
157 # Check type for py2: if not str it's unicode |
|
158 if sys.version_info[0] == 2: |
|
159 try: |
|
160 codestring = codestring.encode('utf-8') |
|
161 except: |
|
162 pass |
|
163 |
|
164 return codestring |
|
165 |
|
166 |
|
167 def extractLineFlags(line, startComment="#", endComment=""): |
|
168 """ |
|
169 Function to extract flags starting and ending with '__' from a line |
|
170 comment. |
|
171 |
|
172 @param line line to extract flags from (string) |
|
173 @keyparam startComment string identifying the start of the comment (string) |
|
174 @keyparam endComment string identifying the end of a comment (string) |
|
175 @return list containing the extracted flags (list of strings) |
|
176 """ |
|
177 flags = [] |
|
178 |
|
179 pos = line.rfind(startComment) |
|
180 if pos >= 0: |
|
181 comment = line[pos + len(startComment):].strip() |
|
182 if endComment: |
|
183 comment = comment.replace("endComment", "") |
|
184 flags = [f.strip() for f in comment.split() |
|
185 if (f.startswith("__") and f.endswith("__"))] |
|
186 return flags |
|
187 |
|
188 |
|
189 def compile_and_check(file_, codestring="", checkFlakes=True, |
|
190 ignoreStarImportWarnings=False): |
|
191 """ |
|
192 Function to compile one Python source file to Python bytecode |
|
193 and to perform a pyflakes check. |
|
194 |
|
195 @param file_ source filename (string) |
|
196 @param codestring string containing the code to compile (string) |
|
197 @keyparam checkFlakes flag indicating to do a pyflakes check (boolean) |
|
198 @keyparam ignoreStarImportWarnings flag indicating to |
|
199 ignore 'star import' warnings (boolean) |
|
200 @return A tuple indicating status (True = an error was found), the |
|
201 file name, the line number, the index number, the code string |
|
202 and the error message (boolean, string, string, string, string, |
|
203 string). If checkFlakes is True, a list of strings containing the |
|
204 warnings (marker, file name, line number, message) |
|
205 The values are only valid, if the status is True. |
|
206 """ |
|
207 try: |
|
208 import builtins |
|
209 except ImportError: |
|
210 import __builtin__ as builtins #__IGNORE_WARNING__ |
|
211 |
|
212 try: |
|
213 if sys.version_info[0] == 2: |
|
214 file_enc = file_.encode(sys.getfilesystemencoding()) |
|
215 else: |
|
216 file_enc = file_ |
|
217 |
|
218 if not codestring: |
|
219 try: |
|
220 codestring = readEncodedFile(file_)[0] |
|
221 except (UnicodeDecodeError, IOError): |
|
222 return (False, None, None, None, None, None, []) |
|
223 |
|
224 codestring = normalizeCode(codestring) |
|
225 |
|
226 if file_.endswith('.ptl'): |
|
227 try: |
|
228 import quixote.ptl_compile |
|
229 except ImportError: |
|
230 return (False, None, None, None, None, None, []) |
|
231 template = quixote.ptl_compile.Template(codestring, file_enc) |
|
232 template.compile() |
|
233 |
|
234 # ast.PyCF_ONLY_AST = 1024, speed optimisation |
|
235 module = builtins.compile(codestring, file_enc, 'exec', 1024) |
|
236 except SyntaxError as detail: |
|
237 index = 0 |
|
238 code = "" |
|
239 error = "" |
|
240 lines = traceback.format_exception_only(SyntaxError, detail) |
|
241 match = re.match('\s*File "(.+)", line (\d+)', |
|
242 lines[0].replace('<string>', '{0}'.format(file_))) |
|
243 if match is not None: |
|
244 fn, line = match.group(1, 2) |
|
245 if lines[1].startswith('SyntaxError:'): |
|
246 error = re.match('SyntaxError: (.+)', lines[1]).group(1) |
|
247 else: |
|
248 code = re.match('(.+)', lines[1]).group(1) |
|
249 for seLine in lines[2:]: |
|
250 if seLine.startswith('SyntaxError:'): |
|
251 error = re.match('SyntaxError: (.+)', seLine).group(1) |
|
252 elif seLine.rstrip().endswith('^'): |
|
253 index = len(seLine.rstrip()) - 4 |
|
254 else: |
|
255 fn = detail.filename |
|
256 line = detail.lineno or 1 |
|
257 error = detail.msg |
|
258 return (True, fn, int(line), index, code, error, []) |
|
259 except ValueError as detail: |
|
260 index = 0 |
|
261 code = "" |
|
262 try: |
|
263 fn = detail.filename |
|
264 line = detail.lineno |
|
265 error = detail.msg |
|
266 except AttributeError: |
|
267 fn = file_ |
|
268 line = 1 |
|
269 error = str(detail) |
|
270 return (True, fn, line, index, code, error, []) |
|
271 except Exception as detail: |
|
272 try: |
|
273 fn = detail.filename |
|
274 line = detail.lineno |
|
275 index = 0 |
|
276 code = "" |
|
277 error = detail.msg |
|
278 return (True, fn, line, index, code, error, []) |
|
279 except: # this catchall is intentional |
|
280 pass |
|
281 |
|
282 # pyflakes |
|
283 if not checkFlakes: |
|
284 return (False, "", -1, -1, "", "", []) |
|
285 |
|
286 strings = [] |
|
287 lines = codestring.splitlines() |
|
288 try: |
|
289 warnings = Checker(module, file_) |
|
290 warnings.messages.sort(key=lambda a: a.lineno) |
|
291 for warning in warnings.messages: |
|
292 if ignoreStarImportWarnings and \ |
|
293 isinstance(warning, ImportStarUsed): |
|
294 continue |
|
295 |
|
296 _fn, lineno, message, msg_args = warning.getMessageData() |
|
297 if "__IGNORE_WARNING__" not in extractLineFlags( |
|
298 lines[lineno - 1].strip()): |
|
299 strings.append([ |
|
300 "FLAKES_WARNING", _fn, lineno, message, msg_args]) |
|
301 except SyntaxError as err: |
|
302 if err.text.strip(): |
|
303 msg = err.text.strip() |
|
304 else: |
|
305 msg = err.msg |
|
306 strings.append(["FLAKES_ERROR", file_, err.lineno, msg, ()]) |
|
307 |
|
308 return (False, "", -1, -1, "", "", strings) |
|
309 |
|
310 |
|
311 if __name__ == "__main__": |
|
312 if len(sys.argv) < 2 or \ |
|
313 len(sys.argv) > 3 or \ |
|
314 (len(sys.argv) == 3 and sys.argv[1] not in ["-fi", "-fs"]): |
|
315 print("ERROR") |
|
316 print("") |
|
317 print("") |
|
318 print("") |
|
319 print("") |
|
320 print("No file name given.") |
|
321 else: |
|
322 filename = sys.argv[-1] |
|
323 checkFlakes = len(sys.argv) == 3 |
|
324 # Setting is ignored if checkFlakes is False |
|
325 ignoreStarImportWarnings = sys.argv[1] == "-fi" |
|
326 |
|
327 try: |
|
328 codestring = readEncodedFile(filename)[0] |
|
329 |
|
330 syntaxerror, fname, line, index, code, error, warnings = \ |
|
331 compile_and_check(filename, codestring, checkFlakes, |
|
332 ignoreStarImportWarnings) |
|
333 except IOError as msg: |
|
334 # fake a syntax error |
|
335 syntaxerror, fname, line, index, code, error, warnings = \ |
|
336 True, filename, 1, 0, "", "I/O Error: %s" % str(msg), [] |
|
337 |
|
338 if syntaxerror: |
|
339 print("ERROR") |
|
340 else: |
|
341 print("NO_ERROR") |
|
342 print(fname) |
|
343 print(line) |
|
344 print(index) |
|
345 print(code) |
|
346 print(error) |
|
347 |
|
348 if not syntaxerror: |
|
349 for warningLine in warnings: |
|
350 msg_args = warningLine.pop() |
|
351 for warning in warningLine: |
|
352 print(warning) |
|
353 msg_args = [str(x) for x in msg_args] |
|
354 print('#'.join(msg_args)) |
|
355 |
|
356 sys.exit(0) |
|