|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2013 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a checker for naming conventions. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals # __IGNORE_WARNING__ |
|
11 |
|
12 import collections |
|
13 import ast |
|
14 import re |
|
15 import os |
|
16 |
|
17 from PyQt4.QtCore import QT_TRANSLATE_NOOP, QCoreApplication |
|
18 |
|
19 |
|
20 class NamingStyleChecker(object): |
|
21 """ |
|
22 Class implementing a checker for naming conventions. |
|
23 """ |
|
24 LowercaseRegex = re.compile(r"[_a-z][_a-z0-9]*$") |
|
25 UppercaseRegexp = re.compile(r"[_A-Z][_A-Z0-9]*$") |
|
26 CamelcaseRegexp = re.compile(r"_?[A-Z][a-zA-Z0-9]*$") |
|
27 MixedcaseRegexp = re.compile(r"_?[a-z][a-zA-Z0-9]*$") |
|
28 |
|
29 Codes = [ |
|
30 "N801", "N802", "N803", "N804", "N805", "N806", "N807", "N808", |
|
31 "N811", "N812", "N813", "N814", "N821", "N831" |
|
32 ] |
|
33 Messages = { |
|
34 "N801": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
35 "class names should use CapWords convention"), |
|
36 "N802": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
37 "function name should be lowercase"), |
|
38 "N803": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
39 "argument name should be lowercase"), |
|
40 "N804": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
41 "first argument of a class method should be named 'cls'"), |
|
42 "N805": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
43 "first argument of a method should be named 'self'"), |
|
44 "N806": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
45 "first argument of a static method should not be named" |
|
46 " 'self' or 'cls"), |
|
47 "N807": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
48 "module names should be lowercase"), |
|
49 "N808": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
50 "package names should be lowercase"), |
|
51 "N811": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
52 "constant imported as non constant"), |
|
53 "N812": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
54 "lowercase imported as non lowercase"), |
|
55 "N813": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
56 "camelcase imported as lowercase"), |
|
57 "N814": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
58 "camelcase imported as constant"), |
|
59 "N821": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
60 "variable in function should be lowercase"), |
|
61 "N831": QT_TRANSLATE_NOOP("NamingStyleChecker", |
|
62 "names 'l', 'O' and 'I' should be avoided"), |
|
63 } |
|
64 |
|
65 def __init__(self, tree, filename, options): |
|
66 """ |
|
67 Constructor (according to 'extended' pep8.py API) |
|
68 |
|
69 @param tree AST tree of the source file |
|
70 @param filename name of the source file (string) |
|
71 @param options options as parsed by pep8.StyleGuide |
|
72 """ |
|
73 self.__parents = collections.deque() |
|
74 self.__tree = tree |
|
75 self.__filename = filename |
|
76 |
|
77 self.__checkersWithCodes = { |
|
78 "classdef": [ |
|
79 (self.__checkClassName, ("N801",)), |
|
80 (self.__checkNameToBeAvoided, ("N831",)), |
|
81 ], |
|
82 "functiondef": [ |
|
83 (self.__checkFuntionName, ("N802",)), |
|
84 (self.__checkFunctionArgumentNames, |
|
85 ("N803", "N804", "N805", "N806")), |
|
86 (self.__checkNameToBeAvoided, ("N831",)), |
|
87 ], |
|
88 "assign": [ |
|
89 (self.__checkVariablesInFunction, ("N821",)), |
|
90 (self.__checkNameToBeAvoided, ("N831",)), |
|
91 ], |
|
92 "importfrom": [ |
|
93 (self.__checkImportAs, ("N811", "N812", "N813", "N814")), |
|
94 ], |
|
95 "module": [ |
|
96 (self.__checkModule, ("N807", "N808")), |
|
97 ], |
|
98 } |
|
99 |
|
100 self.__checkers = {} |
|
101 for key, checkers in self.__checkersWithCodes.items(): |
|
102 for checker, codes in checkers: |
|
103 if any(not (code and options.ignore_code(code)) |
|
104 for code in codes): |
|
105 if key not in self.__checkers: |
|
106 self.__checkers[key] = [] |
|
107 self.__checkers[key].append(checker) |
|
108 |
|
109 def run(self): |
|
110 """ |
|
111 Public method run by the pep8.py checker. |
|
112 |
|
113 @return tuple giving line number, offset within line, code and |
|
114 checker function |
|
115 """ |
|
116 if self.__tree and self.__checkers: |
|
117 return self.__visitTree(self.__tree) |
|
118 else: |
|
119 return () |
|
120 |
|
121 @classmethod |
|
122 def getMessage(cls, code, *args): |
|
123 """ |
|
124 Class method to get a translated and formatted message for a |
|
125 given code. |
|
126 |
|
127 @param code message code (string) |
|
128 @param args arguments for a formatted message (list) |
|
129 @return translated and formatted message (string) |
|
130 """ |
|
131 if code in cls.Messages: |
|
132 return code + " " + QCoreApplication.translate( |
|
133 "NamingStyleChecker", |
|
134 cls.Messages[code]).format(*args) |
|
135 else: |
|
136 return code + " " + QCoreApplication.translate( |
|
137 "NamingStyleChecker", |
|
138 "no message for this code defined") |
|
139 |
|
140 def __visitTree(self, node): |
|
141 """ |
|
142 Private method to scan the given AST tree. |
|
143 |
|
144 @param node AST tree node to scan |
|
145 @return tuple giving line number, offset within line, code and |
|
146 checker function |
|
147 """ |
|
148 for error in self.__visitNode(node): |
|
149 yield error |
|
150 self.__parents.append(node) |
|
151 for child in ast.iter_child_nodes(node): |
|
152 for error in self.__visitTree(child): |
|
153 yield error |
|
154 self.__parents.pop() |
|
155 |
|
156 def __visitNode(self, node): |
|
157 """ |
|
158 Private method to inspect the given AST node. |
|
159 |
|
160 @param node AST tree node to inspect |
|
161 @return tuple giving line number, offset within line, code and |
|
162 checker function |
|
163 """ |
|
164 if isinstance(node, ast.ClassDef): |
|
165 self.__tagClassFunctions(node) |
|
166 elif isinstance(node, ast.FunctionDef): |
|
167 self.__findGlobalDefs(node) |
|
168 |
|
169 checkerName = node.__class__.__name__.lower() |
|
170 if checkerName in self.__checkers: |
|
171 for checker in self.__checkers[checkerName]: |
|
172 for error in checker(node, self.__parents): |
|
173 yield error + (self.__checkers[checkerName],) |
|
174 |
|
175 def __tagClassFunctions(self, classNode): |
|
176 """ |
|
177 Private method to tag functions if they are methods, class methods or |
|
178 static methods. |
|
179 |
|
180 @param classNode AST tree node to tag |
|
181 """ |
|
182 # try to find all 'old style decorators' like |
|
183 # m = staticmethod(m) |
|
184 lateDecoration = {} |
|
185 for node in ast.iter_child_nodes(classNode): |
|
186 if not (isinstance(node, ast.Assign) and |
|
187 isinstance(node.value, ast.Call) and |
|
188 isinstance(node.value.func, ast.Name)): |
|
189 continue |
|
190 funcName = node.value.func.id |
|
191 if funcName in ("classmethod", "staticmethod"): |
|
192 meth = (len(node.value.args) == 1 and node.value.args[0]) |
|
193 if isinstance(meth, ast.Name): |
|
194 lateDecoration[meth.id] = funcName |
|
195 |
|
196 # iterate over all functions and tag them |
|
197 for node in ast.iter_child_nodes(classNode): |
|
198 if not isinstance(node, ast.FunctionDef): |
|
199 continue |
|
200 |
|
201 node.function_type = 'method' |
|
202 if node.name == "__new__": |
|
203 node.function_type = "classmethod" |
|
204 |
|
205 if node.name in lateDecoration: |
|
206 node.function_type = lateDecoration[node.name] |
|
207 elif node.decorator_list: |
|
208 names = [d.id for d in node.decorator_list |
|
209 if isinstance(d, ast.Name) and |
|
210 d.id in ("classmethod", "staticmethod")] |
|
211 if names: |
|
212 node.function_type = names[0] |
|
213 |
|
214 def __findGlobalDefs(self, functionNode): |
|
215 """ |
|
216 Private method amend a node with global definitions information. |
|
217 |
|
218 @param functionNode AST tree node to amend |
|
219 """ |
|
220 globalNames = set() |
|
221 nodesToCheck = collections.deque(ast.iter_child_nodes(functionNode)) |
|
222 while nodesToCheck: |
|
223 node = nodesToCheck.pop() |
|
224 if isinstance(node, ast.Global): |
|
225 globalNames.update(node.names) |
|
226 |
|
227 if not isinstance(node, (ast.FunctionDef, ast.ClassDef)): |
|
228 nodesToCheck.extend(ast.iter_child_nodes(node)) |
|
229 functionNode.global_names = globalNames |
|
230 |
|
231 def __getArgNames(self, node): |
|
232 """ |
|
233 Private method to get the argument names of a function node. |
|
234 |
|
235 @param node AST node to extract arguments names from |
|
236 @return list of argument names (list of string) |
|
237 """ |
|
238 posArgs = [arg.arg for arg in node.args.args] |
|
239 kwOnly = [arg.arg for arg in node.args.kwonlyargs] |
|
240 return posArgs + kwOnly |
|
241 |
|
242 def __error(self, node, code): |
|
243 """ |
|
244 Private method to build the error information. |
|
245 |
|
246 @param node AST node to report an error for |
|
247 @param code error code to report (string) |
|
248 @return tuple giving line number, offset within line and error code |
|
249 (integer, integer, string) |
|
250 """ |
|
251 if isinstance(node, ast.Module): |
|
252 lineno = 0 |
|
253 offset = 0 |
|
254 else: |
|
255 lineno = node.lineno |
|
256 offset = node.col_offset |
|
257 if isinstance(node, ast.ClassDef): |
|
258 lineno += len(node.decorator_list) |
|
259 offset += 6 |
|
260 elif isinstance(node, ast.FunctionDef): |
|
261 lineno += len(node.decorator_list) |
|
262 offset += 4 |
|
263 return (lineno, offset, code) |
|
264 |
|
265 def __isNameToBeAvoided(self, name): |
|
266 """ |
|
267 Private method to check, if the given name should be avoided. |
|
268 |
|
269 @param name name to be checked (string) |
|
270 @return flag indicating to avoid it (boolen) |
|
271 """ |
|
272 return name in ("l", "O", "I") |
|
273 |
|
274 def __checkNameToBeAvoided(self, node, parents): |
|
275 """ |
|
276 Private class to check the given node for a name to be avoided (N831). |
|
277 |
|
278 @param node AST note to check |
|
279 @param parents list of parent nodes |
|
280 @return tuple giving line number, offset within line and error code |
|
281 (integer, integer, string) |
|
282 """ |
|
283 if isinstance(node, (ast.ClassDef, ast.FunctionDef)): |
|
284 name = node.name |
|
285 if self.__isNameToBeAvoided(name): |
|
286 yield self.__error(node, "N831") |
|
287 return |
|
288 |
|
289 if isinstance(node, ast.FunctionDef): |
|
290 argNames = self.__getArgNames(node) |
|
291 for arg in argNames: |
|
292 if self.__isNameToBeAvoided(arg): |
|
293 yield self.__error(node, "N831") |
|
294 return |
|
295 |
|
296 if isinstance(node, ast.Assign): |
|
297 for target in node.targets: |
|
298 name = isinstance(target, ast.Name) and target.id |
|
299 if not name: |
|
300 return |
|
301 |
|
302 if self.__isNameToBeAvoided(name): |
|
303 yield self.__error(node, "N831") |
|
304 return |
|
305 |
|
306 def __checkClassName(self, node, parents): |
|
307 """ |
|
308 Private class to check the given node for class name |
|
309 conventions (N801). |
|
310 |
|
311 Almost without exception, class names use the CapWords convention. |
|
312 Classes for internal use have a leading underscore in addition. |
|
313 |
|
314 @param node AST note to check |
|
315 @param parents list of parent nodes |
|
316 @return tuple giving line number, offset within line and error code |
|
317 (integer, integer, string) |
|
318 """ |
|
319 if not self.CamelcaseRegexp.match(node.name): |
|
320 yield self.__error(node, "N801") |
|
321 |
|
322 def __checkFuntionName(self, node, parents): |
|
323 """ |
|
324 Private class to check the given node for function name |
|
325 conventions (N802). |
|
326 |
|
327 Function names should be lowercase, with words separated by underscores |
|
328 as necessary to improve readability. Functions <b>not</b> being |
|
329 methods '__' in front and back are not allowed. Mixed case is allowed |
|
330 only in contexts where that's already the prevailing style |
|
331 (e.g. threading.py), to retain backwards compatibility. |
|
332 |
|
333 @param node AST note to check |
|
334 @param parents list of parent nodes |
|
335 @return tuple giving line number, offset within line and error code |
|
336 (integer, integer, string) |
|
337 """ |
|
338 functionType = getattr(node, "function_type", "function") |
|
339 name = node.name |
|
340 if (functionType == "function" and "__" in (name[:2], name[-2:])) or \ |
|
341 not self.LowercaseRegex.match(name): |
|
342 yield self.__error(node, "N802") |
|
343 |
|
344 def __checkFunctionArgumentNames(self, node, parents): |
|
345 """ |
|
346 Private class to check the argument names of functions |
|
347 (N803, N804, N805, N806). |
|
348 |
|
349 The argument names of a function should be lowercase, with words |
|
350 separated by underscores. A class method should have 'cls' as the |
|
351 first argument. A method should have 'self' as the first argument. |
|
352 |
|
353 @param node AST note to check |
|
354 @param parents list of parent nodes |
|
355 @return tuple giving line number, offset within line and error code |
|
356 (integer, integer, string) |
|
357 """ |
|
358 if node.args.kwarg is not None: |
|
359 if not self.LowercaseRegex.match(node.args.kwarg): |
|
360 yield self.__error(node, "N803") |
|
361 return |
|
362 |
|
363 if node.args.vararg is not None: |
|
364 if not self.LowercaseRegex.match(node.args.vararg): |
|
365 yield self.__error(node, "N803") |
|
366 return |
|
367 |
|
368 argNames = self.__getArgNames(node) |
|
369 functionType = getattr(node, "function_type", "function") |
|
370 |
|
371 if not argNames: |
|
372 if functionType == "method": |
|
373 yield self.__error(node, "N805") |
|
374 elif functionType == "classmethod": |
|
375 yield self.__error(node, "N804") |
|
376 return |
|
377 |
|
378 if functionType == "method": |
|
379 if argNames[0] != "self": |
|
380 yield self.__error(node, "N805") |
|
381 elif functionType == "classmethod": |
|
382 if argNames[0] != "cls": |
|
383 yield self.__error(node, "N804") |
|
384 elif functionType == "staticmethod": |
|
385 if argNames[0] in ("cls", "self"): |
|
386 yield self.__error(node, "N806") |
|
387 for arg in argNames: |
|
388 if not self.LowercaseRegex.match(arg): |
|
389 yield self.__error(node, "N803") |
|
390 return |
|
391 |
|
392 def __checkVariablesInFunction(self, node, parents): |
|
393 """ |
|
394 Private method to check local variables in functions (N821). |
|
395 |
|
396 Local variables in functions should be lowercase. |
|
397 |
|
398 @param node AST note to check |
|
399 @param parents list of parent nodes |
|
400 @return tuple giving line number, offset within line and error code |
|
401 (integer, integer, string) |
|
402 """ |
|
403 for parentFunc in reversed(parents): |
|
404 if isinstance(parentFunc, ast.ClassDef): |
|
405 return |
|
406 if isinstance(parentFunc, ast.FunctionDef): |
|
407 break |
|
408 else: |
|
409 return |
|
410 for target in node.targets: |
|
411 name = isinstance(target, ast.Name) and target.id |
|
412 if not name or name in parentFunc.global_names: |
|
413 return |
|
414 |
|
415 if not self.LowercaseRegex.match(name) and name[:1] != '_': |
|
416 yield self.__error(target, "N821") |
|
417 |
|
418 def __checkModule(self, node, parents): |
|
419 """ |
|
420 Private method to check module naming conventions (N807, N808). |
|
421 |
|
422 Module and package names should be lowercase. |
|
423 |
|
424 @param node AST note to check |
|
425 @param parents list of parent nodes |
|
426 @return tuple giving line number, offset within line and error code |
|
427 (integer, integer, string) |
|
428 """ |
|
429 if self.__filename: |
|
430 moduleName = os.path.splitext(os.path.basename(self.__filename))[0] |
|
431 if moduleName.lower() != moduleName: |
|
432 yield self.__error(node, "N807") |
|
433 |
|
434 if moduleName == "__init__": |
|
435 # we got a package |
|
436 packageName = \ |
|
437 os.path.split(os.path.dirname(self.__filename))[1] |
|
438 if packageName.lower != packageName: |
|
439 yield self.__error(node, "N808") |
|
440 |
|
441 def __checkImportAs(self, node, parents): |
|
442 """ |
|
443 Private method to check that imports don't change the |
|
444 naming convention (N811, N812, N813, N814). |
|
445 |
|
446 @param node AST note to check |
|
447 @param parents list of parent nodes |
|
448 @return tuple giving line number, offset within line and error code |
|
449 (integer, integer, string) |
|
450 """ |
|
451 for name in node.names: |
|
452 if not name.asname: |
|
453 continue |
|
454 |
|
455 if self.UppercaseRegexp.match(name.name): |
|
456 if not self.UppercaseRegexp.match(name.asname): |
|
457 yield self.__error(node, "N811") |
|
458 elif self.LowercaseRegex.match(name.name): |
|
459 if not self.LowercaseRegex.match(name.asname): |
|
460 yield self.__error(node, "N812") |
|
461 elif self.LowercaseRegex.match(name.asname): |
|
462 yield self.__error(node, "N813") |
|
463 elif self.UppercaseRegexp.match(name.asname): |
|
464 yield self.__error(node, "N814") |