src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Naming/NamingStyleChecker.py

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

eric ide

mercurial