|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the syntax check for Python 2/3. |
|
8 """ |
|
9 from __future__ import unicode_literals |
|
10 |
|
11 import re |
|
12 import sys |
|
13 import traceback |
|
14 |
|
15 from .pyflakes.checker import Checker |
|
16 from .pyflakes.messages import ImportStarUsed |
|
17 |
|
18 |
|
19 def normalizeCode(codestring): |
|
20 """ |
|
21 Function to normalize the given code. |
|
22 |
|
23 @param codestring code to be normalized (string) |
|
24 @return normalized code (string) |
|
25 """ |
|
26 codestring = codestring.replace("\r\n", "\n").replace("\r", "\n") |
|
27 |
|
28 if codestring and codestring[-1] != '\n': |
|
29 codestring = codestring + '\n' |
|
30 |
|
31 # Check type for py2: if not str it's unicode |
|
32 if sys.version_info[0] == 2: |
|
33 try: |
|
34 codestring = codestring.encode('utf-8') |
|
35 except UnicodeError: |
|
36 pass |
|
37 |
|
38 return codestring |
|
39 |
|
40 |
|
41 def extractLineFlags(line, startComment="#", endComment=""): |
|
42 """ |
|
43 Function to extract flags starting and ending with '__' from a line |
|
44 comment. |
|
45 |
|
46 @param line line to extract flags from (string) |
|
47 @keyparam startComment string identifying the start of the comment (string) |
|
48 @keyparam endComment string identifying the end of a comment (string) |
|
49 @return list containing the extracted flags (list of strings) |
|
50 """ |
|
51 flags = [] |
|
52 |
|
53 pos = line.rfind(startComment) |
|
54 if pos >= 0: |
|
55 comment = line[pos + len(startComment):].strip() |
|
56 if endComment: |
|
57 comment = comment.replace("endComment", "") |
|
58 flags = [f.strip() for f in comment.split() |
|
59 if (f.startswith("__") and f.endswith("__"))] |
|
60 return flags |
|
61 |
|
62 |
|
63 def syntaxAndPyflakesCheck(filename, codestring="", checkFlakes=True, |
|
64 ignoreStarImportWarnings=False): |
|
65 """ |
|
66 Function to compile one Python source file to Python bytecode |
|
67 and to perform a pyflakes check. |
|
68 |
|
69 @param filename source filename (string) |
|
70 @keyparam codestring string containing the code to compile (string) |
|
71 @keyparam checkFlakes flag indicating to do a pyflakes check (boolean) |
|
72 @keyparam ignoreStarImportWarnings flag indicating to |
|
73 ignore 'star import' warnings (boolean) |
|
74 @return A tuple indicating status (True = an error was found), the |
|
75 file name, the line number, the index number, the code string |
|
76 and the error message (boolean, string, string, string, string, |
|
77 string). If checkFlakes is True, a list of strings containing the |
|
78 warnings (marker, file name, line number, message) |
|
79 The values are only valid, if the status is True. |
|
80 """ |
|
81 try: |
|
82 import builtins |
|
83 except ImportError: |
|
84 import __builtin__ as builtins #__IGNORE_WARNING__ |
|
85 |
|
86 try: |
|
87 if sys.version_info[0] == 2: |
|
88 file_enc = filename.encode(sys.getfilesystemencoding()) |
|
89 else: |
|
90 file_enc = filename |
|
91 |
|
92 # It also encoded the code back to avoid 'Encoding declaration in |
|
93 # unicode string' exception on Python2 |
|
94 codestring = normalizeCode(codestring) |
|
95 |
|
96 if filename.endswith('.ptl'): |
|
97 try: |
|
98 import quixote.ptl_compile |
|
99 except ImportError: |
|
100 return (True, filename, 0, 0, '', |
|
101 'Quixote plugin not found.', []) |
|
102 template = quixote.ptl_compile.Template(codestring, file_enc) |
|
103 template.compile() |
|
104 |
|
105 # ast.PyCF_ONLY_AST = 1024, speed optimisation |
|
106 module = builtins.compile(codestring, file_enc, 'exec', 1024) |
|
107 except SyntaxError as detail: |
|
108 index = 0 |
|
109 code = "" |
|
110 error = "" |
|
111 lines = traceback.format_exception_only(SyntaxError, detail) |
|
112 if sys.version_info[0] == 2: |
|
113 lines = [x.decode(sys.getfilesystemencoding()) for x in lines] |
|
114 match = re.match('\s*File "(.+)", line (\d+)', |
|
115 lines[0].replace('<string>', '{0}'.format(filename))) |
|
116 if match is not None: |
|
117 fn, line = match.group(1, 2) |
|
118 if lines[1].startswith('SyntaxError:'): |
|
119 error = re.match('SyntaxError: (.+)', lines[1]).group(1) |
|
120 else: |
|
121 code = re.match('(.+)', lines[1]).group(1) |
|
122 for seLine in lines[2:]: |
|
123 if seLine.startswith('SyntaxError:'): |
|
124 error = re.match('SyntaxError: (.+)', seLine).group(1) |
|
125 elif seLine.rstrip().endswith('^'): |
|
126 index = len(seLine.rstrip()) - 4 |
|
127 else: |
|
128 fn = detail.filename |
|
129 line = detail.lineno or 1 |
|
130 error = detail.msg |
|
131 return (True, fn, int(line), index, code, error, []) |
|
132 except ValueError as detail: |
|
133 index = 0 |
|
134 code = "" |
|
135 try: |
|
136 fn = detail.filename |
|
137 line = detail.lineno |
|
138 error = detail.msg |
|
139 except AttributeError: |
|
140 fn = filename |
|
141 line = 1 |
|
142 error = str(detail) |
|
143 return (True, fn, line, index, code, error, []) |
|
144 except Exception as detail: |
|
145 try: |
|
146 fn = detail.filename |
|
147 line = detail.lineno |
|
148 index = 0 |
|
149 code = "" |
|
150 error = detail.msg |
|
151 return (True, fn, line, index, code, error, []) |
|
152 except: # this catchall is intentional |
|
153 pass |
|
154 |
|
155 # pyflakes |
|
156 if not checkFlakes: |
|
157 return (False, "", -1, -1, "", "", []) |
|
158 |
|
159 strings = [] |
|
160 lines = codestring.splitlines() |
|
161 try: |
|
162 warnings = Checker(module, filename) |
|
163 warnings.messages.sort(key=lambda a: a.lineno) |
|
164 for warning in warnings.messages: |
|
165 if ignoreStarImportWarnings and \ |
|
166 isinstance(warning, ImportStarUsed): |
|
167 continue |
|
168 |
|
169 _fn, lineno, message, msg_args = warning.getMessageData() |
|
170 if "__IGNORE_WARNING__" not in extractLineFlags( |
|
171 lines[lineno - 1].strip()): |
|
172 strings.append([ |
|
173 "FLAKES_WARNING", _fn, lineno, message, msg_args]) |
|
174 except SyntaxError as err: |
|
175 if err.text.strip(): |
|
176 msg = err.text.strip() |
|
177 else: |
|
178 msg = err.msg |
|
179 strings.append(["FLAKES_ERROR", filename, err.lineno, msg, ()]) |
|
180 |
|
181 return (False, "", -1, -1, "", "", strings) |