src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/NameOrder/NameOrderChecker.py

branch
eric7
changeset 10046
35b27af462ef
child 10052
041d0785dd42
equal deleted inserted replaced
10045:f5c57f8d17a4 10046:35b27af462ef
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2021 - 2023 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a checker for import statements.
8 """
9
10 import ast
11 import copy
12 import re
13
14
15 class NameOrderChecker:
16 """
17 Class implementing a checker for name ordering.
18
19 Note: Name ordering is checked for import statements, the '__all__' statement
20 and exception names of exception handlers.
21 """
22
23 Codes = [
24 ## Imports order
25 "NO101",
26 "NO102",
27 "NO103",
28 "NO104",
29 "NO105",
30 ]
31
32 def __init__(self, source, filename, tree, select, ignore, expected, repeat, args):
33 """
34 Constructor
35
36 @param source source code to be checked
37 @type list of str
38 @param filename name of the source file
39 @type str
40 @param tree AST tree of the source code
41 @type ast.Module
42 @param select list of selected codes
43 @type list of str
44 @param ignore list of codes to be ignored
45 @type list of str
46 @param expected list of expected codes
47 @type list of str
48 @param repeat flag indicating to report each occurrence of a code
49 @type bool
50 @param args dictionary of arguments for the various checks
51 @type dict
52 """
53 self.__select = tuple(select)
54 self.__ignore = ("",) if select else tuple(ignore)
55 self.__expected = expected[:]
56 self.__repeat = repeat
57 self.__filename = filename
58 self.__source = source[:]
59 self.__tree = copy.deepcopy(tree)
60 self.__args = args
61
62 # parameters for import sorting
63 if args["SortOrder"] == "native":
64 self.__sortingFunction = sorted
65 else:
66 # naturally is the default sort order
67 self.__sortingFunction = self.__naturally
68 self.__sortCaseSensitive = args["SortCaseSensitive"]
69
70 # statistics counters
71 self.counters = {}
72
73 # collection of detected errors
74 self.errors = []
75
76 checkersWithCodes = [
77 (self.__checkNameOrder, ("NO101", "NO102", "NO103", "NO104", "NO105")),
78 ]
79
80 self.__checkers = []
81 for checker, codes in checkersWithCodes:
82 if any(not (code and self.__ignoreCode(code)) for code in codes):
83 self.__checkers.append(checker)
84
85 def __ignoreCode(self, code):
86 """
87 Private method to check if the message code should be ignored.
88
89 @param code message code to check for
90 @type str
91 @return flag indicating to ignore the given code
92 @rtype bool
93 """
94 return code.startswith(self.__ignore) and not code.startswith(self.__select)
95
96 def __error(self, lineNumber, offset, code, *args):
97 """
98 Private method to record an issue.
99
100 @param lineNumber line number of the issue
101 @type int
102 @param offset position within line of the issue
103 @type int
104 @param code message code
105 @type str
106 @param args arguments for the message
107 @type list
108 """
109 if self.__ignoreCode(code):
110 return
111
112 if code in self.counters:
113 self.counters[code] += 1
114 else:
115 self.counters[code] = 1
116
117 # Don't care about expected codes
118 if code in self.__expected:
119 return
120
121 if code and (self.counters[code] == 1 or self.__repeat):
122 # record the issue with one based line number
123 self.errors.append(
124 {
125 "file": self.__filename,
126 "line": lineNumber + 1,
127 "offset": offset,
128 "code": code,
129 "args": args,
130 }
131 )
132
133 def run(self):
134 """
135 Public method to check the given source against miscellaneous
136 conditions.
137 """
138 if not self.__filename:
139 # don't do anything, if essential data is missing
140 return
141
142 if not self.__checkers:
143 # don't do anything, if no codes were selected
144 return
145
146 for check in self.__checkers:
147 check()
148
149 #######################################################################
150 ## Name Order
151 ##
152 ## adapted from: flake8-alphabetize v0.0.21
153 #######################################################################
154
155 def __checkNameOrder(self):
156 """
157 Private method to check the order of import statements.
158 """
159 from .ImportNode import ImportNode
160
161 errors = []
162 imports = []
163 importNodes, aListNode, eListNodes = self.__findNodes(self.__tree)
164
165 # check for an error in '__all__'
166 allError = self.__findErrorInAll(aListNode)
167 if allError is not None:
168 errors.append(allError)
169
170 errors.extend(self.__findExceptionListErrors(eListNodes))
171
172 for importNode in importNodes:
173 if isinstance(importNode, ast.Import) and len(importNode.names) > 1:
174 # skip suck imports because its already handled by pycodestyle
175 continue
176
177 imports.append(
178 ImportNode(
179 self.__args.get("ApplicationPackageNames", []),
180 importNode,
181 self,
182 self.__args.get("SortIgnoringStyle", False),
183 self.__args.get("SortFromFirst", False),
184 )
185 )
186
187 lenImports = len(imports)
188 if lenImports > 0:
189 p = imports[0]
190 if p.error is not None:
191 errors.append(p.error)
192
193 if lenImports > 1:
194 for n in imports[1:]:
195 if n.error is not None:
196 errors.append(n.error)
197
198 if n == p:
199 if self.__args.get("CombinedAsImports", False) or (
200 not n.asImport and not p.asImport
201 ):
202 errors.append((n.node, "NO103", str(p), str(n)))
203 elif n < p:
204 errors.append((n.node, "NO101", str(n), str(p)))
205
206 p = n
207
208 for error in errors:
209 if not self.__ignoreCode(error[1]):
210 node = error[0]
211 reason = error[1]
212 args = error[2:]
213 self.__error(node.lineno - 1, node.col_offset, reason, *args)
214
215 def __findExceptionListNodes(self, tree):
216 """
217 Private method to find all exception types handled by given tree.
218
219 @param tree reference to the ast node tree to be parsed
220 @type ast.AST
221 @return list of exception types
222 @rtype list of ast.Name
223 """
224 nodes = []
225
226 for node in ast.walk(tree):
227 if isinstance(node, ast.ExceptHandler):
228 nodeType = node.type
229 if isinstance(nodeType, (ast.List, ast.Tuple)):
230 nodes.append(nodeType)
231
232 return nodes
233
234 def __findNodes(self, tree):
235 """
236 Private method to find all import and import from nodes of the given
237 tree.
238
239 @param tree reference to the ast node tree to be parsed
240 @type ast.AST
241 @return tuple containing a list of import nodes, the '__all__' node and
242 exception nodes
243 @rtype tuple of (ast.Import | ast.ImportFrom, ast.List | ast.Tuple,
244 ast.List | ast.Tuple)
245 """
246 importNodes = []
247 aListNode = None
248 eListNodes = self.__findExceptionListNodes(tree)
249
250 if isinstance(tree, ast.Module):
251 body = tree.body
252
253 for n in body:
254 if isinstance(n, (ast.Import, ast.ImportFrom)):
255 importNodes.append(n)
256
257 elif isinstance(n, ast.Assign):
258 for t in n.targets:
259 if isinstance(t, ast.Name) and t.id == "__all__":
260 value = n.value
261
262 if isinstance(value, (ast.List, ast.Tuple)):
263 aListNode = value
264
265 return importNodes, aListNode, eListNodes
266
267 def __findErrorInAll(self, node):
268 """
269 Private method to check the '__all__' node for errors.
270
271 @param node reference to the '__all__' node
272 @type ast.List or ast.Tuple
273 @return tuple containing a reference to the node an error code and the error
274 arguments
275 @rtype tuple of (ast.List | ast.Tuple, str, str)
276 """
277 if node is not None:
278 actualList = []
279 for el in node.elts:
280 if isinstance(el, ast.Constant):
281 actualList.append(el.value)
282 elif isinstance(el, ast.Str):
283 actualList.append(el.s)
284 else:
285 # Can't handle anything that isn't a string literal
286 return None
287
288 expectedList = self.sorted(
289 actualList,
290 key=lambda k: self.moduleKey(k, subImports=True),
291 )
292 if expectedList != actualList:
293 return (node, "NO104", ", ".join(expectedList))
294
295 return None
296
297 def __findExceptionListStr(self, node):
298 """
299 Private method to get the exception name out of an exception handler type node.
300
301 @param node node to be treated
302 @type ast.Name or ast.Attribute
303 @return string containing the exception name
304 @rtype str
305 """
306 if isinstance(node, ast.Name):
307 return node.id
308 elif isinstance(node, ast.Attribute):
309 return f"{self.__findExceptionListStr(node.value)}.{node.attr}"
310
311 return ""
312
313 def __findExceptionListErrors(self, nodes):
314 """
315 Private method to check the exception node for errors.
316
317 @param nodes list of exception nodes
318 @type list of ast.List or ast.Tuple
319 @return DESCRIPTION
320 @rtype TYPE
321 """
322 errors = []
323
324 for node in nodes:
325 actualList = [self.__findExceptionListStr(elt) for elt in node.elts]
326
327 expectedList = self.sorted(actualList)
328 if expectedList != actualList:
329 errors.append((node, "NO105", ", ".join(expectedList)))
330
331 return errors
332
333 def sorted(self, toSort, key=None, reverse=False):
334 """
335 Public method to sort the given list of names.
336
337 @param toSort list of names to be sorted
338 @type list of str
339 @param key function to generate keys (defaults to None)
340 @type function (optional)
341 @param reverse flag indicating a reverse sort (defaults to False)
342 @type bool (optional)
343 @return sorted list of names
344 @rtype list of str
345 """
346 return self.__sortingFunction(toSort, key=key, reverse=reverse)
347
348 def __naturally(self, toSort, key=None, reverse=False):
349 """
350 Private method to sort the given list of names naturally.
351
352 Note: Natural sorting maintains the sort order of numbers (i.e.
353 [Q1, Q10, Q2] is sorted as [Q1, Q2, Q10] while the Python
354 standard sort would yield [Q1, Q10, Q2].
355
356 @param toSort list of names to be sorted
357 @type list of str
358 @param key function to generate keys (defaults to None)
359 @type function (optional)
360 @param reverse flag indicating a reverse sort (defaults to False)
361 @type bool (optional)
362 @return sorted list of names
363 @rtype list of str
364 """
365 if key is None:
366 keyCallback = self.__naturalKeys
367 else:
368
369 def keyCallback(text):
370 return self.__naturalKeys(key(text))
371
372 return sorted(toSort, key=keyCallback, reverse=reverse)
373
374 def __atoi(self, text):
375 """
376 Private method to convert the given text to an integer number.
377
378 @param text text to be converted
379 @type str
380 @return integer number
381 @rtype int
382 """
383 return int(text) if text.isdigit() else text
384
385 def __naturalKeys(self, text):
386 """
387 Private method to generate keys for natural sorting.
388
389 @param text text to generate a key for
390 @type str
391 @return key for natural sorting
392 @rtype list of str or int
393 """
394 return [self.__atoi(c) for c in re.split(r"(\d+)", text)]
395
396 def moduleKey(self, moduleName, subImports=False):
397 """
398 Public method to generate a key for the given module name.
399
400 @param moduleName module name
401 @type str
402 @param subImports flag indicating a sub import like in
403 'from foo import bar, baz' (defaults to False)
404 @type bool (optional)
405 @return generated key
406 @rtype str
407 """
408 prefix = ""
409
410 if subImports:
411 if moduleName.isupper() and len(moduleName) > 1:
412 prefix = "A"
413 elif moduleName[0:1].isupper():
414 prefix = "B"
415 else:
416 prefix = "C"
417 if not self.__sortCaseSensitive:
418 moduleName = moduleName.lower()
419
420 return f"{prefix}{moduleName}"

eric ide

mercurial