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