6 """ |
6 """ |
7 Module implementing a checker for import statements. |
7 Module implementing a checker for import statements. |
8 """ |
8 """ |
9 |
9 |
10 import ast |
10 import ast |
11 import copy |
|
12 import re |
11 import re |
13 |
12 |
14 |
13 from CodeStyleTopicChecker import CodeStyleTopicChecker |
15 class NameOrderChecker: |
14 |
|
15 |
|
16 class NameOrderChecker(CodeStyleTopicChecker): |
16 """ |
17 """ |
17 Class implementing a checker for name ordering. |
18 Class implementing a checker for name ordering. |
18 |
19 |
19 Note: Name ordering is checked for import statements, the '__all__' statement |
20 Note: Name ordering is checked for import statements, the '__all__' statement |
20 and exception names of exception handlers. |
21 and exception names of exception handlers. |
21 """ |
22 """ |
22 |
23 |
23 Codes = [ |
24 Codes = [ |
24 ## Imports order |
25 ## Imports order |
25 "NO101", |
26 "NO-101", |
26 "NO102", |
27 "NO-102", |
27 "NO103", |
28 "NO-103", |
28 "NO104", |
29 "NO-104", |
29 "NO105", |
30 "NO-105", |
30 ] |
31 ] |
31 Prefix = "NO" |
32 Category = "NO" |
32 |
33 |
33 def __init__(self, source, filename, tree, select, ignore, expected, repeat, args): |
34 def __init__(self, source, filename, tree, select, ignore, expected, repeat, args): |
34 """ |
35 """ |
35 Constructor |
36 Constructor |
36 |
37 |
49 @param repeat flag indicating to report each occurrence of a code |
50 @param repeat flag indicating to report each occurrence of a code |
50 @type bool |
51 @type bool |
51 @param args dictionary of arguments for the various checks |
52 @param args dictionary of arguments for the various checks |
52 @type dict |
53 @type dict |
53 """ |
54 """ |
54 self.__select = tuple( |
55 super().__init__( |
55 x for x in select if x.startswith(NameOrderChecker.Prefix) |
56 NameOrderChecker.Category, |
|
57 source, |
|
58 filename, |
|
59 tree, |
|
60 select, |
|
61 ignore, |
|
62 expected, |
|
63 repeat, |
|
64 args, |
56 ) |
65 ) |
57 self.__ignore = tuple( |
|
58 x for x in ignore if x.startswith(NameOrderChecker.Prefix) |
|
59 ) |
|
60 self.__expected = expected[:] |
|
61 self.__repeat = repeat |
|
62 self.__filename = filename |
|
63 self.__source = source[:] |
|
64 self.__tree = copy.deepcopy(tree) |
|
65 self.__args = args |
|
66 |
66 |
67 # parameters for import sorting |
67 # parameters for import sorting |
68 if args["SortOrder"] == "native": |
68 if args["SortOrder"] == "native": |
69 self.__sortingFunction = sorted |
69 self.__sortingFunction = sorted |
70 else: |
70 else: |
71 # naturally is the default sort order |
71 # naturally is the default sort order |
72 self.__sortingFunction = self.__naturally |
72 self.__sortingFunction = self.__naturally |
73 self.__sortCaseSensitive = args["SortCaseSensitive"] |
73 self.__sortCaseSensitive = args["SortCaseSensitive"] |
74 |
74 |
75 # statistics counters |
|
76 self.counters = {} |
|
77 |
|
78 # collection of detected errors |
|
79 self.errors = [] |
|
80 |
|
81 checkersWithCodes = [ |
75 checkersWithCodes = [ |
82 (self.__checkNameOrder, ("NO101", "NO102", "NO103", "NO104", "NO105")), |
76 (self.__checkNameOrder, ("NO-101", "NO-102", "NO-103", "NO-104", "NO-105")), |
83 ] |
77 ] |
84 |
78 self._initializeCheckers(checkersWithCodes) |
85 self.__checkers = [] |
|
86 for checker, codes in checkersWithCodes: |
|
87 if any(not (code and self.__ignoreCode(code)) for code in codes): |
|
88 self.__checkers.append(checker) |
|
89 |
|
90 def __ignoreCode(self, code): |
|
91 """ |
|
92 Private method to check if the message code should be ignored. |
|
93 |
|
94 @param code message code to check for |
|
95 @type str |
|
96 @return flag indicating to ignore the given code |
|
97 @rtype bool |
|
98 """ |
|
99 return code in self.__ignore or ( |
|
100 code.startswith(self.__ignore) and not code.startswith(self.__select) |
|
101 ) |
|
102 |
|
103 def __error(self, lineNumber, offset, code, *args): |
|
104 """ |
|
105 Private method to record an issue. |
|
106 |
|
107 @param lineNumber line number of the issue |
|
108 @type int |
|
109 @param offset position within line of the issue |
|
110 @type int |
|
111 @param code message code |
|
112 @type str |
|
113 @param args arguments for the message |
|
114 @type list |
|
115 """ |
|
116 if self.__ignoreCode(code): |
|
117 return |
|
118 |
|
119 if code in self.counters: |
|
120 self.counters[code] += 1 |
|
121 else: |
|
122 self.counters[code] = 1 |
|
123 |
|
124 # Don't care about expected codes |
|
125 if code in self.__expected: |
|
126 return |
|
127 |
|
128 if code and (self.counters[code] == 1 or self.__repeat): |
|
129 # record the issue with one based line number |
|
130 self.errors.append( |
|
131 { |
|
132 "file": self.__filename, |
|
133 "line": lineNumber + 1, |
|
134 "offset": offset, |
|
135 "code": code, |
|
136 "args": args, |
|
137 } |
|
138 ) |
|
139 |
|
140 def run(self): |
|
141 """ |
|
142 Public method to check the given source against miscellaneous |
|
143 conditions. |
|
144 """ |
|
145 if not self.__filename: |
|
146 # don't do anything, if essential data is missing |
|
147 return |
|
148 |
|
149 if not self.__checkers: |
|
150 # don't do anything, if no codes were selected |
|
151 return |
|
152 |
|
153 for check in self.__checkers: |
|
154 check() |
|
155 |
79 |
156 ####################################################################### |
80 ####################################################################### |
157 ## Name Order |
81 ## Name Order |
158 ## |
82 ## |
159 ## adapted from: flake8-alphabetize v0.0.21 |
83 ## adapted from: flake8-alphabetize v0.0.21 |
201 for n in imports[1:]: |
125 for n in imports[1:]: |
202 if n.error is not None: |
126 if n.error is not None: |
203 errors.append(n.error) |
127 errors.append(n.error) |
204 |
128 |
205 if n == p: |
129 if n == p: |
206 if self.__args.get("CombinedAsImports", False) or ( |
130 if self.args.get("CombinedAsImports", False) or ( |
207 not n.asImport and not p.asImport |
131 not n.asImport and not p.asImport |
208 ): |
132 ): |
209 errors.append((n.node, "NO103", str(p), str(n))) |
133 errors.append((n.node, "NO-103", str(p), str(n))) |
210 elif n < p: |
134 elif n < p: |
211 errors.append((n.node, "NO101", str(n), str(p))) |
135 errors.append((n.node, "NO-101", str(n), str(p))) |
212 |
136 |
213 p = n |
137 p = n |
214 |
138 |
215 for error in errors: |
139 for error in errors: |
216 if not self.__ignoreCode(error[1]): |
140 self.addErrorFromNode(error[0], error[1], *error[2:]) |
217 node = error[0] |
|
218 reason = error[1] |
|
219 args = error[2:] |
|
220 self.__error(node.lineno - 1, node.col_offset, reason, *args) |
|
221 |
141 |
222 def __findExceptionListNodes(self, tree): |
142 def __findExceptionListNodes(self, tree): |
223 """ |
143 """ |
224 Private method to find all exception types handled by given tree. |
144 Private method to find all exception types handled by given tree. |
225 |
145 |
329 for node in nodes: |
249 for node in nodes: |
330 actualList = [self.__findExceptionListStr(elt) for elt in node.elts] |
250 actualList = [self.__findExceptionListStr(elt) for elt in node.elts] |
331 |
251 |
332 expectedList = self.sorted(actualList) |
252 expectedList = self.sorted(actualList) |
333 if expectedList != actualList: |
253 if expectedList != actualList: |
334 errors.append((node, "NO105", ", ".join(expectedList))) |
254 errors.append((node, "NO-105", ", ".join(expectedList))) |
335 |
255 |
336 return errors |
256 return errors |
337 |
257 |
338 def sorted(self, toSort, key=None, reverse=False): |
258 def sorted(self, toSort, key=None, reverse=False): |
339 """ |
259 """ |