eric6/Plugins/CheckerPlugins/CodeStyleChecker/Naming/NamingStyleChecker.py

changeset 7784
3257703e10c5
parent 7639
422fd05e9c91
child 7894
4370a8b30648
child 7924
8a96736d465e
equal deleted inserted replaced
7783:36f66ce496bd 7784:3257703e10c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2013 - 2020 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'
135 # like 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 posArgs = [arg.arg for arg in node.args.args]
192 kwOnly = [arg.arg for arg in node.args.kwonlyargs]
193 return posArgs + kwOnly
194
195 def __error(self, node, code):
196 """
197 Private method to build the error information.
198
199 @param node AST node to report an error for
200 @param code error code to report (string)
201 @return tuple giving line number, offset within line and error code
202 (integer, integer, string)
203 """
204 if isinstance(node, ast.Module):
205 lineno = 0
206 offset = 0
207 else:
208 lineno = node.lineno
209 offset = node.col_offset
210 if isinstance(node, ast.ClassDef):
211 lineno += len(node.decorator_list)
212 offset += 6
213 elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
214 lineno += len(node.decorator_list)
215 offset += 4
216 return (lineno, offset, code)
217
218 def __isNameToBeAvoided(self, name):
219 """
220 Private method to check, if the given name should be avoided.
221
222 @param name name to be checked (string)
223 @return flag indicating to avoid it (boolen)
224 """
225 return name in ("l", "O", "I")
226
227 def __checkNameToBeAvoided(self, node, parents):
228 """
229 Private class to check the given node for a name to be avoided (N831).
230
231 @param node AST note to check
232 @param parents list of parent nodes
233 @return tuple giving line number, offset within line and error code
234 (integer, integer, string)
235 """
236 if isinstance(node, (ast.ClassDef, ast.FunctionDef,
237 ast.AsyncFunctionDef)):
238 name = node.name
239 if self.__isNameToBeAvoided(name):
240 yield self.__error(node, "N831")
241 return
242
243 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
244 argNames = self.__getArgNames(node)
245 for arg in argNames:
246 if self.__isNameToBeAvoided(arg):
247 yield self.__error(node, "N831")
248 return
249
250 if isinstance(node, ast.Assign):
251 for target in node.targets:
252 if isinstance(target, ast.Name):
253 name = target.id
254 if not name:
255 return
256
257 if self.__isNameToBeAvoided(name):
258 yield self.__error(node, "N831")
259 return
260
261 elif isinstance(target, (ast.Tuple, ast.List)):
262 for element in target.elts:
263 if isinstance(element, ast.Name):
264 name = element.id
265 if not name:
266 return
267
268 if self.__isNameToBeAvoided(name):
269 yield self.__error(node, "N831")
270 return
271
272 def __checkClassName(self, node, parents):
273 """
274 Private class to check the given node for class name
275 conventions (N801).
276
277 Almost without exception, class names use the CapWords convention.
278 Classes for internal use have a leading underscore in addition.
279
280 @param node AST note to check
281 @param parents list of parent nodes
282 @return tuple giving line number, offset within line and error code
283 (integer, integer, string)
284 """
285 if not self.CamelcaseRegexp.match(node.name):
286 yield self.__error(node, "N801")
287
288 def __checkFunctionName(self, node, parents):
289 """
290 Private class to check the given node for function name
291 conventions (N802).
292
293 Function names should be lowercase, with words separated by underscores
294 as necessary to improve readability. Functions <b>not</b> being
295 methods '__' in front and back are not allowed. Mixed case is allowed
296 only in contexts where that's already the prevailing style
297 (e.g. threading.py), to retain backwards compatibility.
298
299 @param node AST note to check
300 @param parents list of parent nodes
301 @return tuple giving line number, offset within line and error code
302 (integer, integer, string)
303 """
304 functionType = getattr(node, "function_type", "function")
305 name = node.name
306 if (
307 (functionType == "function" and "__" in (name[:2], name[-2:])) or
308 not self.LowercaseRegex.match(name)
309 ):
310 yield self.__error(node, "N802")
311
312 def __checkFunctionArgumentNames(self, node, parents):
313 """
314 Private class to check the argument names of functions
315 (N803, N804, N805, N806).
316
317 The argument names of a function should be lowercase, with words
318 separated by underscores. A class method should have 'cls' as the
319 first argument. A method should have 'self' as the first argument.
320
321 @param node AST note to check
322 @param parents list of parent nodes
323 @return tuple giving line number, offset within line and error code
324 (integer, integer, string)
325 """
326 if node.args.kwarg is not None:
327 if sys.version_info >= (3, 4):
328 kwarg = node.args.kwarg.arg
329 else:
330 kwarg = node.args.kwarg
331 if not self.LowercaseRegex.match(kwarg):
332 yield self.__error(node, "N803")
333 return
334
335 if node.args.vararg is not None:
336 if sys.version_info >= (3, 4):
337 vararg = node.args.vararg.arg
338 else:
339 vararg = node.args.vararg
340 if not self.LowercaseRegex.match(vararg):
341 yield self.__error(node, "N803")
342 return
343
344 argNames = self.__getArgNames(node)
345 functionType = getattr(node, "function_type", "function")
346
347 if not argNames:
348 if functionType == "method":
349 yield self.__error(node, "N805")
350 elif functionType == "classmethod":
351 yield self.__error(node, "N804")
352 return
353
354 if functionType == "method":
355 if argNames[0] != "self":
356 yield self.__error(node, "N805")
357 elif functionType == "classmethod":
358 if argNames[0] != "cls":
359 yield self.__error(node, "N804")
360 elif functionType == "staticmethod":
361 if argNames[0] in ("cls", "self"):
362 yield self.__error(node, "N806")
363 for arg in argNames:
364 if not self.LowercaseRegex.match(arg):
365 yield self.__error(node, "N803")
366 return
367
368 def __checkVariablesInFunction(self, node, parents):
369 """
370 Private method to check local variables in functions (N821).
371
372 Local variables in functions should be lowercase.
373
374 @param node AST note to check
375 @param parents list of parent nodes
376 @return tuple giving line number, offset within line and error code
377 (integer, integer, string)
378 """
379 for parentFunc in reversed(parents):
380 if isinstance(parentFunc, ast.ClassDef):
381 return
382 if isinstance(parentFunc, (ast.FunctionDef, ast.AsyncFunctionDef)):
383 break
384 else:
385 return
386 for target in node.targets:
387 name = isinstance(target, ast.Name) and target.id
388 if not name or name in parentFunc.global_names:
389 return
390
391 if not self.LowercaseRegex.match(name) and name[:1] != '_':
392 yield self.__error(target, "N821")
393
394 def __checkModule(self, node, parents):
395 """
396 Private method to check module naming conventions (N807, N808).
397
398 Module and package names should be lowercase.
399
400 @param node AST note to check
401 @param parents list of parent nodes
402 @return tuple giving line number, offset within line and error code
403 (integer, integer, string)
404 """
405 if self.__filename:
406 moduleName = os.path.splitext(os.path.basename(self.__filename))[0]
407 if moduleName.lower() != moduleName:
408 yield self.__error(node, "N807")
409
410 if moduleName == "__init__":
411 # we got a package
412 packageName = (
413 os.path.split(os.path.dirname(self.__filename))[1]
414 )
415 if packageName.lower() != packageName:
416 yield self.__error(node, "N808")
417
418 def __checkImportAs(self, node, parents):
419 """
420 Private method to check that imports don't change the
421 naming convention (N811, N812, N813, N814).
422
423 @param node AST note to check
424 @param parents list of parent nodes
425 @return tuple giving line number, offset within line and error code
426 (integer, integer, string)
427 """
428 for name in node.names:
429 if not name.asname:
430 continue
431
432 if self.UppercaseRegexp.match(name.name):
433 if not self.UppercaseRegexp.match(name.asname):
434 yield self.__error(node, "N811")
435 elif self.LowercaseRegex.match(name.name):
436 if not self.LowercaseRegex.match(name.asname):
437 yield self.__error(node, "N812")
438 elif self.LowercaseRegex.match(name.asname):
439 yield self.__error(node, "N813")
440 elif self.UppercaseRegexp.match(name.asname):
441 yield self.__error(node, "N814")

eric ide

mercurial