eric6/Plugins/CheckerPlugins/CodeStyleChecker/NamingStyleChecker.py

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

eric ide

mercurial