|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2015 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a checker for miscellaneous checks. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import ast |
|
12 import re |
|
13 import itertools |
|
14 from string import Formatter |
|
15 from collections import defaultdict |
|
16 import tokenize |
|
17 import copy |
|
18 import contextlib |
|
19 |
|
20 import AstUtilities |
|
21 |
|
22 from .eradicate import Eradicator |
|
23 |
|
24 from .MiscellaneousDefaults import MiscellaneousCheckerDefaultArgs |
|
25 |
|
26 |
|
27 def composeCallPath(node): |
|
28 """ |
|
29 Generator function to assemble the call path of a given node. |
|
30 |
|
31 @param node node to assemble call path for |
|
32 @type ast.Node |
|
33 @yield call path components |
|
34 @ytype str |
|
35 """ |
|
36 if isinstance(node, ast.Attribute): |
|
37 yield from composeCallPath(node.value) |
|
38 yield node.attr |
|
39 elif isinstance(node, ast.Name): |
|
40 yield node.id |
|
41 |
|
42 |
|
43 class MiscellaneousChecker: |
|
44 """ |
|
45 Class implementing a checker for miscellaneous checks. |
|
46 """ |
|
47 Codes = [ |
|
48 ## Coding line |
|
49 "M101", "M102", |
|
50 |
|
51 ## Copyright |
|
52 "M111", "M112", |
|
53 |
|
54 ## Shadowed Builtins |
|
55 "M131", "M132", |
|
56 |
|
57 ## Comprehensions |
|
58 "M181", "M182", "M183", "M184", "M185", "M186", "M187", "M188", |
|
59 "M189", |
|
60 "M191", "M192", "M193", "M195", "M196", "M197", "M198", |
|
61 |
|
62 ## Dictionaries with sorted keys |
|
63 "M201", |
|
64 |
|
65 ## Naive datetime usage |
|
66 "M301", "M302", "M303", "M304", "M305", "M306", "M307", "M308", |
|
67 "M311", "M312", "M313", "M314", "M315", |
|
68 "M321", |
|
69 |
|
70 ## sys.version and sys.version_info usage |
|
71 "M401", "M402", "M403", |
|
72 "M411", "M412", "M413", "M414", |
|
73 "M421", "M422", "M423", |
|
74 |
|
75 ## Bugbear |
|
76 "M501", "M502", "M503", "M504", "M505", "M506", "M507", "M508", |
|
77 "M509", |
|
78 "M511", "M512", "M513", |
|
79 "M521", "M522", "M523", "M524", |
|
80 |
|
81 ## Format Strings |
|
82 "M601", |
|
83 "M611", "M612", "M613", |
|
84 "M621", "M622", "M623", "M624", "M625", |
|
85 "M631", "M632", |
|
86 |
|
87 ## Logging |
|
88 "M651", "M652", "M653", "M654", "M655", |
|
89 |
|
90 ## Future statements |
|
91 "M701", "M702", |
|
92 |
|
93 ## Gettext |
|
94 "M711", |
|
95 |
|
96 ## print |
|
97 "M801", |
|
98 |
|
99 ## one element tuple |
|
100 "M811", |
|
101 |
|
102 ## Mutable Defaults |
|
103 "M821", "M822", |
|
104 |
|
105 ## return statements |
|
106 "M831", "M832", "M833", "M834", |
|
107 |
|
108 ## line continuation |
|
109 "M841", |
|
110 |
|
111 ## commented code |
|
112 "M891", |
|
113 ] |
|
114 |
|
115 Formatter = Formatter() |
|
116 FormatFieldRegex = re.compile(r'^((?:\s|.)*?)(\..*|\[.*\])?$') |
|
117 |
|
118 BuiltinsWhiteList = [ |
|
119 "__name__", |
|
120 "__doc__", |
|
121 "credits", |
|
122 ] |
|
123 |
|
124 def __init__(self, source, filename, tree, select, ignore, expected, |
|
125 repeat, args): |
|
126 """ |
|
127 Constructor |
|
128 |
|
129 @param source source code to be checked |
|
130 @type list of str |
|
131 @param filename name of the source file |
|
132 @type str |
|
133 @param tree AST tree of the source code |
|
134 @type ast.Module |
|
135 @param select list of selected codes |
|
136 @type list of str |
|
137 @param ignore list of codes to be ignored |
|
138 @type list of str |
|
139 @param expected list of expected codes |
|
140 @type list of str |
|
141 @param repeat flag indicating to report each occurrence of a code |
|
142 @type bool |
|
143 @param args dictionary of arguments for the miscellaneous checks |
|
144 @type dict |
|
145 """ |
|
146 self.__select = tuple(select) |
|
147 self.__ignore = ('',) if select else tuple(ignore) |
|
148 self.__expected = expected[:] |
|
149 self.__repeat = repeat |
|
150 self.__filename = filename |
|
151 self.__source = source[:] |
|
152 self.__tree = copy.deepcopy(tree) |
|
153 self.__args = args |
|
154 |
|
155 self.__pep3101FormatRegex = re.compile( |
|
156 r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%') |
|
157 |
|
158 import builtins |
|
159 self.__builtins = [b for b in dir(builtins) |
|
160 if b not in self.BuiltinsWhiteList] |
|
161 |
|
162 self.__eradicator = Eradicator() |
|
163 |
|
164 # statistics counters |
|
165 self.counters = {} |
|
166 |
|
167 # collection of detected errors |
|
168 self.errors = [] |
|
169 |
|
170 checkersWithCodes = [ |
|
171 (self.__checkCoding, ("M101", "M102")), |
|
172 (self.__checkCopyright, ("M111", "M112")), |
|
173 (self.__checkBuiltins, ("M131", "M132")), |
|
174 (self.__checkComprehensions, ("M181", "M182", "M183", "M184", |
|
175 "M185", "M186", "M187", "M188", |
|
176 "M189", |
|
177 "M191", "M192", "M193", |
|
178 "M195", "M196", "M197", "M198")), |
|
179 (self.__checkDictWithSortedKeys, ("M201",)), |
|
180 (self.__checkDateTime, ("M301", "M302", "M303", "M304", "M305", |
|
181 "M306", "M307", "M308", "M311", "M312", |
|
182 "M313", "M314", "M315", "M321")), |
|
183 (self.__checkSysVersion, ("M401", "M402", "M403", |
|
184 "M411", "M412", "M413", "M414", |
|
185 "M421", "M422", "M423")), |
|
186 (self.__checkBugBear, ("M501", "M502", "M503", "M504", "M505", |
|
187 "M506", "M507", "M508", "M509", |
|
188 "M511", "M512", "M513", |
|
189 "M521", "M522", "M523", "M524")), |
|
190 (self.__checkPep3101, ("M601",)), |
|
191 (self.__checkFormatString, ("M611", "M612", "M613", |
|
192 "M621", "M622", "M623", "M624", "M625", |
|
193 "M631", "M632")), |
|
194 (self.__checkLogging, ("M651", "M652", "M653", "M654", "M655")), |
|
195 (self.__checkFuture, ("M701", "M702")), |
|
196 (self.__checkGettext, ("M711",)), |
|
197 (self.__checkPrintStatements, ("M801",)), |
|
198 (self.__checkTuple, ("M811",)), |
|
199 (self.__checkMutableDefault, ("M821", "M822")), |
|
200 (self.__checkReturn, ("M831", "M832", "M833", "M834")), |
|
201 (self.__checkLineContinuation, ("M841",)), |
|
202 (self.__checkCommentedCode, ("M891",)), |
|
203 ] |
|
204 |
|
205 # the eradicate whitelist |
|
206 commentedCodeCheckerArgs = self.__args.get( |
|
207 "CommentedCodeChecker", |
|
208 MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"]) |
|
209 commentedCodeCheckerWhitelist = commentedCodeCheckerArgs.get( |
|
210 "WhiteList", |
|
211 MiscellaneousCheckerDefaultArgs[ |
|
212 "CommentedCodeChecker"]["WhiteList"]) |
|
213 self.__eradicator.update_whitelist(commentedCodeCheckerWhitelist, |
|
214 extend_default=False) |
|
215 |
|
216 self.__checkers = [] |
|
217 for checker, codes in checkersWithCodes: |
|
218 if any(not (code and self.__ignoreCode(code)) |
|
219 for code in codes): |
|
220 self.__checkers.append(checker) |
|
221 |
|
222 def __ignoreCode(self, code): |
|
223 """ |
|
224 Private method to check if the message code should be ignored. |
|
225 |
|
226 @param code message code to check for |
|
227 @type str |
|
228 @return flag indicating to ignore the given code |
|
229 @rtype bool |
|
230 """ |
|
231 return (code.startswith(self.__ignore) and |
|
232 not code.startswith(self.__select)) |
|
233 |
|
234 def __error(self, lineNumber, offset, code, *args): |
|
235 """ |
|
236 Private method to record an issue. |
|
237 |
|
238 @param lineNumber line number of the issue |
|
239 @type int |
|
240 @param offset position within line of the issue |
|
241 @type int |
|
242 @param code message code |
|
243 @type str |
|
244 @param args arguments for the message |
|
245 @type list |
|
246 """ |
|
247 if self.__ignoreCode(code): |
|
248 return |
|
249 |
|
250 if code in self.counters: |
|
251 self.counters[code] += 1 |
|
252 else: |
|
253 self.counters[code] = 1 |
|
254 |
|
255 # Don't care about expected codes |
|
256 if code in self.__expected: |
|
257 return |
|
258 |
|
259 if code and (self.counters[code] == 1 or self.__repeat): |
|
260 # record the issue with one based line number |
|
261 self.errors.append( |
|
262 { |
|
263 "file": self.__filename, |
|
264 "line": lineNumber + 1, |
|
265 "offset": offset, |
|
266 "code": code, |
|
267 "args": args, |
|
268 } |
|
269 ) |
|
270 |
|
271 def run(self): |
|
272 """ |
|
273 Public method to check the given source against miscellaneous |
|
274 conditions. |
|
275 """ |
|
276 if not self.__filename: |
|
277 # don't do anything, if essential data is missing |
|
278 return |
|
279 |
|
280 if not self.__checkers: |
|
281 # don't do anything, if no codes were selected |
|
282 return |
|
283 |
|
284 for check in self.__checkers: |
|
285 check() |
|
286 |
|
287 def __getCoding(self): |
|
288 """ |
|
289 Private method to get the defined coding of the source. |
|
290 |
|
291 @return tuple containing the line number and the coding |
|
292 @rtype tuple of int and str |
|
293 """ |
|
294 for lineno, line in enumerate(self.__source[:5]): |
|
295 matched = re.search(r'coding[:=]\s*([-\w_.]+)', |
|
296 line, re.IGNORECASE) |
|
297 if matched: |
|
298 return lineno, matched.group(1) |
|
299 else: |
|
300 return 0, "" |
|
301 |
|
302 def __checkCoding(self): |
|
303 """ |
|
304 Private method to check the presence of a coding line and valid |
|
305 encodings. |
|
306 """ |
|
307 if len(self.__source) == 0: |
|
308 return |
|
309 |
|
310 encodings = [e.lower().strip() |
|
311 for e in self.__args.get( |
|
312 "CodingChecker", |
|
313 MiscellaneousCheckerDefaultArgs["CodingChecker"]) |
|
314 .split(",")] |
|
315 lineno, coding = self.__getCoding() |
|
316 if coding: |
|
317 if coding.lower() not in encodings: |
|
318 self.__error(lineno, 0, "M102", coding) |
|
319 else: |
|
320 self.__error(0, 0, "M101") |
|
321 |
|
322 def __checkCopyright(self): |
|
323 """ |
|
324 Private method to check the presence of a copyright statement. |
|
325 """ |
|
326 source = "".join(self.__source) |
|
327 copyrightArgs = self.__args.get( |
|
328 "CopyrightChecker", |
|
329 MiscellaneousCheckerDefaultArgs["CopyrightChecker"]) |
|
330 copyrightMinFileSize = copyrightArgs.get( |
|
331 "MinFilesize", |
|
332 MiscellaneousCheckerDefaultArgs["CopyrightChecker"]["MinFilesize"]) |
|
333 copyrightAuthor = copyrightArgs.get( |
|
334 "Author", |
|
335 MiscellaneousCheckerDefaultArgs["CopyrightChecker"]["Author"]) |
|
336 copyrightRegexStr = ( |
|
337 r"Copyright\s+(\(C\)\s+)?(\d{{4}}\s+-\s+)?\d{{4}}\s+{author}" |
|
338 ) |
|
339 |
|
340 tocheck = max(1024, copyrightMinFileSize) |
|
341 topOfSource = source[:tocheck] |
|
342 if len(topOfSource) < copyrightMinFileSize: |
|
343 return |
|
344 |
|
345 copyrightRe = re.compile(copyrightRegexStr.format(author=r".*"), |
|
346 re.IGNORECASE) |
|
347 if not copyrightRe.search(topOfSource): |
|
348 self.__error(0, 0, "M111") |
|
349 return |
|
350 |
|
351 if copyrightAuthor: |
|
352 copyrightAuthorRe = re.compile( |
|
353 copyrightRegexStr.format(author=copyrightAuthor), |
|
354 re.IGNORECASE) |
|
355 if not copyrightAuthorRe.search(topOfSource): |
|
356 self.__error(0, 0, "M112") |
|
357 |
|
358 def __checkCommentedCode(self): |
|
359 """ |
|
360 Private method to check for commented code. |
|
361 """ |
|
362 source = "".join(self.__source) |
|
363 commentedCodeCheckerArgs = self.__args.get( |
|
364 "CommentedCodeChecker", |
|
365 MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"]) |
|
366 aggressive = commentedCodeCheckerArgs.get( |
|
367 "Aggressive", |
|
368 MiscellaneousCheckerDefaultArgs[ |
|
369 "CommentedCodeChecker"]["Aggressive"]) |
|
370 for markedLine in self.__eradicator.commented_out_code_line_numbers( |
|
371 source, aggressive=aggressive): |
|
372 self.__error(markedLine - 1, 0, "M891") |
|
373 |
|
374 def __checkLineContinuation(self): |
|
375 """ |
|
376 Private method to check line continuation using backslash. |
|
377 """ |
|
378 # generate source lines without comments |
|
379 linesIterator = iter(self.__source) |
|
380 tokens = tokenize.generate_tokens(lambda: next(linesIterator)) |
|
381 comments = [token for token in tokens if token[0] == tokenize.COMMENT] |
|
382 stripped = self.__source[:] |
|
383 for comment in comments: |
|
384 lineno = comment[3][0] |
|
385 start = comment[2][1] |
|
386 stop = comment[3][1] |
|
387 content = stripped[lineno - 1] |
|
388 withoutComment = content[:start] + content[stop:] |
|
389 stripped[lineno - 1] = withoutComment.rstrip() |
|
390 |
|
391 # perform check with 'cleaned' source |
|
392 for lineIndex, line in enumerate(stripped): |
|
393 strippedLine = line.strip() |
|
394 if (strippedLine.endswith('\\') and |
|
395 not strippedLine.startswith(('assert', 'with'))): |
|
396 self.__error(lineIndex, len(line), "M841") |
|
397 |
|
398 def __checkPrintStatements(self): |
|
399 """ |
|
400 Private method to check for print statements. |
|
401 """ |
|
402 for node in ast.walk(self.__tree): |
|
403 if ( |
|
404 (isinstance(node, ast.Call) and |
|
405 getattr(node.func, 'id', None) == 'print') or |
|
406 (hasattr(ast, 'Print') and isinstance(node, ast.Print)) |
|
407 ): |
|
408 self.__error(node.lineno - 1, node.col_offset, "M801") |
|
409 |
|
410 def __checkTuple(self): |
|
411 """ |
|
412 Private method to check for one element tuples. |
|
413 """ |
|
414 for node in ast.walk(self.__tree): |
|
415 if ( |
|
416 isinstance(node, ast.Tuple) and |
|
417 len(node.elts) == 1 |
|
418 ): |
|
419 self.__error(node.lineno - 1, node.col_offset, "M811") |
|
420 |
|
421 def __checkFuture(self): |
|
422 """ |
|
423 Private method to check the __future__ imports. |
|
424 """ |
|
425 expectedImports = { |
|
426 i.strip() |
|
427 for i in self.__args.get("FutureChecker", "").split(",") |
|
428 if bool(i.strip())} |
|
429 if len(expectedImports) == 0: |
|
430 # nothing to check for; disabling the check |
|
431 return |
|
432 |
|
433 imports = set() |
|
434 node = None |
|
435 hasCode = False |
|
436 |
|
437 for node in ast.walk(self.__tree): |
|
438 if (isinstance(node, ast.ImportFrom) and |
|
439 node.module == '__future__'): |
|
440 imports |= {name.name for name in node.names} |
|
441 elif isinstance(node, ast.Expr): |
|
442 if not AstUtilities.isString(node.value): |
|
443 hasCode = True |
|
444 break |
|
445 elif not ( |
|
446 AstUtilities.isString(node) or |
|
447 isinstance(node, ast.Module) |
|
448 ): |
|
449 hasCode = True |
|
450 break |
|
451 |
|
452 if isinstance(node, ast.Module) or not hasCode: |
|
453 return |
|
454 |
|
455 if imports < expectedImports: |
|
456 if imports: |
|
457 self.__error(node.lineno - 1, node.col_offset, "M701", |
|
458 ", ".join(expectedImports), ", ".join(imports)) |
|
459 else: |
|
460 self.__error(node.lineno - 1, node.col_offset, "M702", |
|
461 ", ".join(expectedImports)) |
|
462 |
|
463 def __checkPep3101(self): |
|
464 """ |
|
465 Private method to check for old style string formatting. |
|
466 """ |
|
467 for lineno, line in enumerate(self.__source): |
|
468 match = self.__pep3101FormatRegex.search(line) |
|
469 if match: |
|
470 lineLen = len(line) |
|
471 pos = line.find('%') |
|
472 formatPos = pos |
|
473 formatter = '%' |
|
474 if line[pos + 1] == "(": |
|
475 pos = line.find(")", pos) |
|
476 c = line[pos] |
|
477 while c not in "diouxXeEfFgGcrs": |
|
478 pos += 1 |
|
479 if pos >= lineLen: |
|
480 break |
|
481 c = line[pos] |
|
482 if c in "diouxXeEfFgGcrs": |
|
483 formatter += c |
|
484 self.__error(lineno, formatPos, "M601", formatter) |
|
485 |
|
486 def __checkFormatString(self): |
|
487 """ |
|
488 Private method to check string format strings. |
|
489 """ |
|
490 coding = self.__getCoding()[1] |
|
491 if not coding: |
|
492 # default to utf-8 |
|
493 coding = "utf-8" |
|
494 |
|
495 visitor = TextVisitor() |
|
496 visitor.visit(self.__tree) |
|
497 for node in visitor.nodes: |
|
498 text = node.s |
|
499 if isinstance(text, bytes): |
|
500 try: |
|
501 text = text.decode(coding) |
|
502 except UnicodeDecodeError: |
|
503 continue |
|
504 fields, implicit, explicit = self.__getFields(text) |
|
505 if implicit: |
|
506 if node in visitor.calls: |
|
507 self.__error(node.lineno - 1, node.col_offset, "M611") |
|
508 else: |
|
509 if node.is_docstring: |
|
510 self.__error(node.lineno - 1, node.col_offset, "M612") |
|
511 else: |
|
512 self.__error(node.lineno - 1, node.col_offset, "M613") |
|
513 |
|
514 if node in visitor.calls: |
|
515 call, strArgs = visitor.calls[node] |
|
516 |
|
517 numbers = set() |
|
518 names = set() |
|
519 # Determine which fields require a keyword and which an arg |
|
520 for name in fields: |
|
521 fieldMatch = self.FormatFieldRegex.match(name) |
|
522 try: |
|
523 number = int(fieldMatch.group(1)) |
|
524 except ValueError: |
|
525 number = -1 |
|
526 # negative numbers are considered keywords |
|
527 if number >= 0: |
|
528 numbers.add(number) |
|
529 else: |
|
530 names.add(fieldMatch.group(1)) |
|
531 |
|
532 keywords = {keyword.arg for keyword in call.keywords} |
|
533 numArgs = len(call.args) |
|
534 if strArgs: |
|
535 numArgs -= 1 |
|
536 hasKwArgs = any(kw.arg is None for kw in call.keywords) |
|
537 hasStarArgs = sum(1 for arg in call.args |
|
538 if isinstance(arg, ast.Starred)) |
|
539 |
|
540 if hasKwArgs: |
|
541 keywords.discard(None) |
|
542 if hasStarArgs: |
|
543 numArgs -= 1 |
|
544 |
|
545 # if starargs or kwargs is not None, it can't count the |
|
546 # parameters but at least check if the args are used |
|
547 if hasKwArgs and not names: |
|
548 # No names but kwargs |
|
549 self.__error(call.lineno - 1, call.col_offset, "M623") |
|
550 if hasStarArgs and not numbers: |
|
551 # No numbers but args |
|
552 self.__error(call.lineno - 1, call.col_offset, "M624") |
|
553 |
|
554 if not hasKwArgs and not hasStarArgs: |
|
555 # can actually verify numbers and names |
|
556 for number in sorted(numbers): |
|
557 if number >= numArgs: |
|
558 self.__error(call.lineno - 1, call.col_offset, |
|
559 "M621", number) |
|
560 |
|
561 for name in sorted(names): |
|
562 if name not in keywords: |
|
563 self.__error(call.lineno - 1, call.col_offset, |
|
564 "M622", name) |
|
565 |
|
566 for arg in range(numArgs): |
|
567 if arg not in numbers: |
|
568 self.__error(call.lineno - 1, call.col_offset, "M631", |
|
569 arg) |
|
570 |
|
571 for keyword in keywords: |
|
572 if keyword not in names: |
|
573 self.__error(call.lineno - 1, call.col_offset, "M632", |
|
574 keyword) |
|
575 |
|
576 if implicit and explicit: |
|
577 self.__error(call.lineno - 1, call.col_offset, "M625") |
|
578 |
|
579 def __getFields(self, string): |
|
580 """ |
|
581 Private method to extract the format field information. |
|
582 |
|
583 @param string format string to be parsed |
|
584 @type str |
|
585 @return format field information as a tuple with fields, implicit |
|
586 field definitions present and explicit field definitions present |
|
587 @rtype tuple of set of str, bool, bool |
|
588 """ |
|
589 fields = set() |
|
590 cnt = itertools.count() |
|
591 implicit = False |
|
592 explicit = False |
|
593 try: |
|
594 for _literal, field, spec, conv in self.Formatter.parse(string): |
|
595 if field is not None and (conv is None or conv in 'rsa'): |
|
596 if not field: |
|
597 field = str(next(cnt)) |
|
598 implicit = True |
|
599 else: |
|
600 explicit = True |
|
601 fields.add(field) |
|
602 fields.update(parsedSpec[1] |
|
603 for parsedSpec in self.Formatter.parse(spec) |
|
604 if parsedSpec[1] is not None) |
|
605 except ValueError: |
|
606 return set(), False, False |
|
607 else: |
|
608 return fields, implicit, explicit |
|
609 |
|
610 def __checkBuiltins(self): |
|
611 """ |
|
612 Private method to check, if built-ins are shadowed. |
|
613 """ |
|
614 functionDefs = [ast.FunctionDef] |
|
615 with contextlib.suppress(AttributeError): |
|
616 functionDefs.append(ast.AsyncFunctionDef) |
|
617 |
|
618 ignoreBuiltinAssignments = self.__args.get( |
|
619 "BuiltinsChecker", |
|
620 MiscellaneousCheckerDefaultArgs["BuiltinsChecker"]) |
|
621 |
|
622 for node in ast.walk(self.__tree): |
|
623 if isinstance(node, ast.Assign): |
|
624 # assign statement |
|
625 for element in node.targets: |
|
626 if ( |
|
627 isinstance(element, ast.Name) and |
|
628 element.id in self.__builtins |
|
629 ): |
|
630 value = node.value |
|
631 if ( |
|
632 isinstance(value, ast.Name) and |
|
633 element.id in ignoreBuiltinAssignments and |
|
634 value.id in ignoreBuiltinAssignments[element.id] |
|
635 ): |
|
636 # ignore compatibility assignments |
|
637 continue |
|
638 self.__error(element.lineno - 1, element.col_offset, |
|
639 "M131", element.id) |
|
640 elif isinstance(element, (ast.Tuple, ast.List)): |
|
641 for tupleElement in element.elts: |
|
642 if ( |
|
643 isinstance(tupleElement, ast.Name) and |
|
644 tupleElement.id in self.__builtins |
|
645 ): |
|
646 self.__error(tupleElement.lineno - 1, |
|
647 tupleElement.col_offset, |
|
648 "M131", tupleElement.id) |
|
649 elif isinstance(node, ast.For): |
|
650 # for loop |
|
651 target = node.target |
|
652 if ( |
|
653 isinstance(target, ast.Name) and |
|
654 target.id in self.__builtins |
|
655 ): |
|
656 self.__error(target.lineno - 1, target.col_offset, |
|
657 "M131", target.id) |
|
658 elif isinstance(target, (ast.Tuple, ast.List)): |
|
659 for element in target.elts: |
|
660 if ( |
|
661 isinstance(element, ast.Name) and |
|
662 element.id in self.__builtins |
|
663 ): |
|
664 self.__error(element.lineno - 1, |
|
665 element.col_offset, |
|
666 "M131", element.id) |
|
667 elif any(isinstance(node, functionDef) |
|
668 for functionDef in functionDefs): |
|
669 # (asynchronous) function definition |
|
670 for arg in node.args.args: |
|
671 if ( |
|
672 isinstance(arg, ast.arg) and |
|
673 arg.arg in self.__builtins |
|
674 ): |
|
675 self.__error(arg.lineno - 1, arg.col_offset, |
|
676 "M132", arg.arg) |
|
677 |
|
678 def __checkComprehensions(self): |
|
679 """ |
|
680 Private method to check some comprehension related things. |
|
681 """ |
|
682 for node in ast.walk(self.__tree): |
|
683 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): |
|
684 nArgs = len(node.args) |
|
685 nKwArgs = len(node.keywords) |
|
686 |
|
687 if ( |
|
688 nArgs == 1 and |
|
689 isinstance(node.args[0], ast.GeneratorExp) and |
|
690 node.func.id in ('list', 'set') |
|
691 ): |
|
692 errorCode = { |
|
693 "list": "M181", |
|
694 "set": "M182", |
|
695 }[node.func.id] |
|
696 self.__error(node.lineno - 1, node.col_offset, errorCode) |
|
697 |
|
698 elif ( |
|
699 nArgs == 1 and |
|
700 isinstance(node.args[0], |
|
701 (ast.GeneratorExp, ast.ListComp)) and |
|
702 isinstance(node.args[0].elt, ast.Tuple) and |
|
703 len(node.args[0].elt.elts) == 2 and |
|
704 node.func.id == "dict" |
|
705 ): |
|
706 if isinstance(node.args[0], ast.GeneratorExp): |
|
707 errorCode = "M183" |
|
708 else: |
|
709 errorCode = "M185" |
|
710 self.__error(node.lineno - 1, node.col_offset, errorCode) |
|
711 |
|
712 elif ( |
|
713 nArgs == 1 and |
|
714 isinstance(node.args[0], ast.ListComp) and |
|
715 node.func.id in ('list', 'set') |
|
716 ): |
|
717 errorCode = { |
|
718 'list': 'M195', |
|
719 'set': 'M184', |
|
720 }[node.func.id] |
|
721 self.__error(node.lineno - 1, node.col_offset, errorCode) |
|
722 |
|
723 elif nArgs == 1 and ( |
|
724 isinstance(node.args[0], ast.Tuple) and |
|
725 node.func.id == "tuple" or |
|
726 isinstance(node.args[0], ast.List) and |
|
727 node.func.id == "list" |
|
728 ): |
|
729 errorCode = { |
|
730 'tuple': 'M197', |
|
731 'list': 'M198', |
|
732 }[node.func.id] |
|
733 self.__error(node.lineno - 1, node.col_offset, errorCode, |
|
734 type(node.args[0]).__name__.lower(), |
|
735 node.func.id) |
|
736 |
|
737 elif ( |
|
738 nArgs == 1 and |
|
739 isinstance(node.args[0], (ast.Tuple, ast.List)) and |
|
740 node.func.id in ("tuple", "list", "set", "dict") |
|
741 ): |
|
742 errorCode = { |
|
743 "tuple": "M192", |
|
744 "list": "M193", |
|
745 "set": "M191", |
|
746 "dict": "M191", |
|
747 }[node.func.id] |
|
748 self.__error(node.lineno - 1, node.col_offset, errorCode, |
|
749 type(node.args[0]).__name__.lower(), |
|
750 node.func.id) |
|
751 |
|
752 elif ( |
|
753 nArgs == 0 and |
|
754 not any(isinstance(a, ast.Starred) for a in node.args) and |
|
755 not any(k.arg is None for k in node.keywords) and |
|
756 node.func.id == "dict" |
|
757 ) or ( |
|
758 nArgs == 0 and |
|
759 nKwArgs == 0 and |
|
760 node.func.id in ("tuple", "list") |
|
761 ): |
|
762 self.__error(node.lineno - 1, node.col_offset, "M186", |
|
763 node.func.id) |
|
764 |
|
765 elif ( |
|
766 node.func.id in {"list", "reversed"} and |
|
767 nArgs > 0 and |
|
768 isinstance(node.args[0], ast.Call) and |
|
769 isinstance(node.args[0].func, ast.Name) and |
|
770 node.args[0].func.id == "sorted" |
|
771 ): |
|
772 if node.func.id == "reversed": |
|
773 reverseFlagValue = False |
|
774 for kw in node.args[0].keywords: |
|
775 if kw.arg != "reverse": |
|
776 continue |
|
777 if isinstance(kw.value, ast.NameConstant): |
|
778 reverseFlagValue = kw.value.value |
|
779 elif isinstance(kw.value, ast.Num): |
|
780 reverseFlagValue = bool(kw.value.n) |
|
781 else: |
|
782 # Complex value |
|
783 reverseFlagValue = None |
|
784 |
|
785 if reverseFlagValue is None: |
|
786 self.__error(node.lineno - 1, node.col_offset, |
|
787 "M187a", node.func.id, |
|
788 node.args[0].func.id) |
|
789 else: |
|
790 self.__error(node.lineno - 1, node.col_offset, |
|
791 "M187b", node.func.id, |
|
792 node.args[0].func.id, |
|
793 not reverseFlagValue) |
|
794 else: |
|
795 self.__error(node.lineno - 1, node.col_offset, |
|
796 "M187c", node.func.id, |
|
797 node.args[0].func.id) |
|
798 |
|
799 elif ( |
|
800 nArgs > 0 and |
|
801 isinstance(node.args[0], ast.Call) and |
|
802 isinstance(node.args[0].func, ast.Name) and |
|
803 ( |
|
804 ( |
|
805 node.func.id in {"set", "sorted"} and |
|
806 node.args[0].func.id in { |
|
807 "list", "reversed", "sorted", "tuple"} |
|
808 ) or ( |
|
809 node.func.id in {"list", "tuple"} and |
|
810 node.args[0].func.id in {"list", "tuple"} |
|
811 ) or ( |
|
812 node.func.id == "set" and |
|
813 node.args[0].func.id == "set" |
|
814 ) |
|
815 ) |
|
816 ): |
|
817 self.__error(node.lineno - 1, node.col_offset, "M188", |
|
818 node.args[0].func.id, node.func.id) |
|
819 |
|
820 elif ( |
|
821 node.func.id in {"reversed", "set", "sorted"} and |
|
822 nArgs > 0 and |
|
823 isinstance(node.args[0], ast.Subscript) and |
|
824 isinstance(node.args[0].slice, ast.Slice) and |
|
825 node.args[0].slice.lower is None and |
|
826 node.args[0].slice.upper is None and |
|
827 isinstance(node.args[0].slice.step, ast.UnaryOp) and |
|
828 isinstance(node.args[0].slice.step.op, ast.USub) and |
|
829 isinstance(node.args[0].slice.step.operand, ast.Num) and |
|
830 node.args[0].slice.step.operand.n == 1 |
|
831 ): |
|
832 self.__error(node.lineno - 1, node.col_offset, |
|
833 "M189", node.func.id) |
|
834 |
|
835 elif ( |
|
836 isinstance(node, (ast.ListComp, ast.SetComp)) and ( |
|
837 len(node.generators) == 1 and |
|
838 not node.generators[0].ifs and |
|
839 not node.generators[0].is_async and ( |
|
840 isinstance(node.elt, ast.Name) and |
|
841 isinstance(node.generators[0].target, ast.Name) and |
|
842 node.elt.id == node.generators[0].target.id |
|
843 ) |
|
844 ) |
|
845 ): |
|
846 compType = { |
|
847 ast.DictComp: "dict", |
|
848 ast.ListComp: "list", |
|
849 ast.SetComp: "set", |
|
850 }[node.__class__] |
|
851 |
|
852 self.__error(node.lineno - 1, node.col_offset, |
|
853 "M196", compType) |
|
854 |
|
855 def __checkMutableDefault(self): |
|
856 """ |
|
857 Private method to check for use of mutable types as default arguments. |
|
858 """ |
|
859 mutableTypes = ( |
|
860 ast.Call, |
|
861 ast.Dict, |
|
862 ast.List, |
|
863 ast.Set, |
|
864 ) |
|
865 mutableCalls = ( |
|
866 "Counter", |
|
867 "OrderedDict", |
|
868 "collections.Counter", |
|
869 "collections.OrderedDict", |
|
870 "collections.defaultdict", |
|
871 "collections.deque", |
|
872 "defaultdict", |
|
873 "deque", |
|
874 "dict", |
|
875 "list", |
|
876 "set", |
|
877 ) |
|
878 immutableCalls = ( |
|
879 "tuple", |
|
880 "frozenset", |
|
881 ) |
|
882 functionDefs = [ast.FunctionDef] |
|
883 with contextlib.suppress(AttributeError): |
|
884 functionDefs.append(ast.AsyncFunctionDef) |
|
885 |
|
886 for node in ast.walk(self.__tree): |
|
887 if any(isinstance(node, functionDef) |
|
888 for functionDef in functionDefs): |
|
889 defaults = node.args.defaults[:] |
|
890 with contextlib.suppress(AttributeError): |
|
891 defaults += node.args.kw_defaults[:] |
|
892 for default in defaults: |
|
893 if any(isinstance(default, mutableType) |
|
894 for mutableType in mutableTypes): |
|
895 typeName = type(default).__name__ |
|
896 if isinstance(default, ast.Call): |
|
897 callPath = '.'.join(composeCallPath(default.func)) |
|
898 if callPath in mutableCalls: |
|
899 self.__error(default.lineno - 1, |
|
900 default.col_offset, |
|
901 "M823", callPath + "()") |
|
902 elif callPath not in immutableCalls: |
|
903 self.__error(default.lineno - 1, |
|
904 default.col_offset, |
|
905 "M822", typeName) |
|
906 else: |
|
907 self.__error(default.lineno - 1, |
|
908 default.col_offset, |
|
909 "M821", typeName) |
|
910 |
|
911 def __dictShouldBeChecked(self, node): |
|
912 """ |
|
913 Private function to test, if the node should be checked. |
|
914 |
|
915 @param node reference to the AST node |
|
916 @return flag indicating to check the node |
|
917 @rtype bool |
|
918 """ |
|
919 if not all(AstUtilities.isString(key) for key in node.keys): |
|
920 return False |
|
921 |
|
922 if ( |
|
923 "__IGNORE_WARNING__" in self.__source[node.lineno - 1] or |
|
924 "__IGNORE_WARNING_M201__" in self.__source[node.lineno - 1] |
|
925 ): |
|
926 return False |
|
927 |
|
928 lineNumbers = [key.lineno for key in node.keys] |
|
929 return len(lineNumbers) == len(set(lineNumbers)) |
|
930 |
|
931 def __checkDictWithSortedKeys(self): |
|
932 """ |
|
933 Private method to check, if dictionary keys appear in sorted order. |
|
934 """ |
|
935 for node in ast.walk(self.__tree): |
|
936 if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node): |
|
937 for key1, key2 in zip(node.keys, node.keys[1:]): |
|
938 if key2.s < key1.s: |
|
939 self.__error(key2.lineno - 1, key2.col_offset, |
|
940 "M201", key2.s, key1.s) |
|
941 |
|
942 def __checkLogging(self): |
|
943 """ |
|
944 Private method to check logging statements. |
|
945 """ |
|
946 visitor = LoggingVisitor() |
|
947 visitor.visit(self.__tree) |
|
948 for node, reason in visitor.violations: |
|
949 self.__error(node.lineno - 1, node.col_offset, reason) |
|
950 |
|
951 def __checkGettext(self): |
|
952 """ |
|
953 Private method to check the 'gettext' import statement. |
|
954 """ |
|
955 for node in ast.walk(self.__tree): |
|
956 if ( |
|
957 isinstance(node, ast.ImportFrom) and |
|
958 any(name.asname == '_' for name in node.names) |
|
959 ): |
|
960 self.__error(node.lineno - 1, node.col_offset, "M711", |
|
961 node.names[0].name) |
|
962 |
|
963 def __checkBugBear(self): |
|
964 """ |
|
965 Private method for bugbear checks. |
|
966 """ |
|
967 visitor = BugBearVisitor() |
|
968 visitor.visit(self.__tree) |
|
969 for violation in visitor.violations: |
|
970 node = violation[0] |
|
971 reason = violation[1] |
|
972 params = violation[2:] |
|
973 self.__error(node.lineno - 1, node.col_offset, reason, *params) |
|
974 |
|
975 def __checkReturn(self): |
|
976 """ |
|
977 Private method to check return statements. |
|
978 """ |
|
979 visitor = ReturnVisitor() |
|
980 visitor.visit(self.__tree) |
|
981 for violation in visitor.violations: |
|
982 node = violation[0] |
|
983 reason = violation[1] |
|
984 self.__error(node.lineno - 1, node.col_offset, reason) |
|
985 |
|
986 def __checkDateTime(self): |
|
987 """ |
|
988 Private method to check use of naive datetime functions. |
|
989 """ |
|
990 # step 1: generate an augmented node tree containing parent info |
|
991 # for each child node |
|
992 tree = copy.deepcopy(self.__tree) |
|
993 for node in ast.walk(tree): |
|
994 for childNode in ast.iter_child_nodes(node): |
|
995 childNode._dtCheckerParent = node |
|
996 |
|
997 # step 2: perform checks and report issues |
|
998 visitor = DateTimeVisitor() |
|
999 visitor.visit(tree) |
|
1000 for violation in visitor.violations: |
|
1001 node = violation[0] |
|
1002 reason = violation[1] |
|
1003 self.__error(node.lineno - 1, node.col_offset, reason) |
|
1004 |
|
1005 def __checkSysVersion(self): |
|
1006 """ |
|
1007 Private method to check the use of sys.version and sys.version_info. |
|
1008 """ |
|
1009 visitor = SysVersionVisitor() |
|
1010 visitor.visit(self.__tree) |
|
1011 for violation in visitor.violations: |
|
1012 node = violation[0] |
|
1013 reason = violation[1] |
|
1014 self.__error(node.lineno - 1, node.col_offset, reason) |
|
1015 |
|
1016 |
|
1017 class TextVisitor(ast.NodeVisitor): |
|
1018 """ |
|
1019 Class implementing a node visitor for bytes and str instances. |
|
1020 |
|
1021 It tries to detect docstrings as string of the first expression of each |
|
1022 module, class or function. |
|
1023 """ |
|
1024 # modelled after the string format flake8 extension |
|
1025 |
|
1026 def __init__(self): |
|
1027 """ |
|
1028 Constructor |
|
1029 """ |
|
1030 super().__init__() |
|
1031 self.nodes = [] |
|
1032 self.calls = {} |
|
1033 |
|
1034 def __addNode(self, node): |
|
1035 """ |
|
1036 Private method to add a node to our list of nodes. |
|
1037 |
|
1038 @param node reference to the node to add |
|
1039 @type ast.AST |
|
1040 """ |
|
1041 if not hasattr(node, 'is_docstring'): |
|
1042 node.is_docstring = False |
|
1043 self.nodes.append(node) |
|
1044 |
|
1045 def visit_Str(self, node): |
|
1046 """ |
|
1047 Public method to record a string node. |
|
1048 |
|
1049 @param node reference to the string node |
|
1050 @type ast.Str |
|
1051 """ |
|
1052 self.__addNode(node) |
|
1053 |
|
1054 def visit_Bytes(self, node): |
|
1055 """ |
|
1056 Public method to record a bytes node. |
|
1057 |
|
1058 @param node reference to the bytes node |
|
1059 @type ast.Bytes |
|
1060 """ |
|
1061 self.__addNode(node) |
|
1062 |
|
1063 def visit_Constant(self, node): |
|
1064 """ |
|
1065 Public method to handle constant nodes. |
|
1066 |
|
1067 @param node reference to the bytes node |
|
1068 @type ast.Constant |
|
1069 """ |
|
1070 if sys.version_info >= (3, 8, 0): |
|
1071 if AstUtilities.isBaseString(node): |
|
1072 self.__addNode(node) |
|
1073 else: |
|
1074 super().generic_visit(node) |
|
1075 else: |
|
1076 super().generic_visit(node) |
|
1077 |
|
1078 def __visitDefinition(self, node): |
|
1079 """ |
|
1080 Private method handling class and function definitions. |
|
1081 |
|
1082 @param node reference to the node to handle |
|
1083 @type ast.FunctionDef, ast.AsyncFunctionDef or ast.ClassDef |
|
1084 """ |
|
1085 # Manually traverse class or function definition |
|
1086 # * Handle decorators normally |
|
1087 # * Use special check for body content |
|
1088 # * Don't handle the rest (e.g. bases) |
|
1089 for decorator in node.decorator_list: |
|
1090 self.visit(decorator) |
|
1091 self.__visitBody(node) |
|
1092 |
|
1093 def __visitBody(self, node): |
|
1094 """ |
|
1095 Private method to traverse the body of the node manually. |
|
1096 |
|
1097 If the first node is an expression which contains a string or bytes it |
|
1098 marks that as a docstring. |
|
1099 |
|
1100 @param node reference to the node to traverse |
|
1101 @type ast.AST |
|
1102 """ |
|
1103 if ( |
|
1104 node.body and |
|
1105 isinstance(node.body[0], ast.Expr) and |
|
1106 AstUtilities.isBaseString(node.body[0].value) |
|
1107 ): |
|
1108 node.body[0].value.is_docstring = True |
|
1109 |
|
1110 for subnode in node.body: |
|
1111 self.visit(subnode) |
|
1112 |
|
1113 def visit_Module(self, node): |
|
1114 """ |
|
1115 Public method to handle a module. |
|
1116 |
|
1117 @param node reference to the node to handle |
|
1118 @type ast.Module |
|
1119 """ |
|
1120 self.__visitBody(node) |
|
1121 |
|
1122 def visit_ClassDef(self, node): |
|
1123 """ |
|
1124 Public method to handle a class definition. |
|
1125 |
|
1126 @param node reference to the node to handle |
|
1127 @type ast.ClassDef |
|
1128 """ |
|
1129 # Skipped nodes: ('name', 'bases', 'keywords', 'starargs', 'kwargs') |
|
1130 self.__visitDefinition(node) |
|
1131 |
|
1132 def visit_FunctionDef(self, node): |
|
1133 """ |
|
1134 Public method to handle a function definition. |
|
1135 |
|
1136 @param node reference to the node to handle |
|
1137 @type ast.FunctionDef |
|
1138 """ |
|
1139 # Skipped nodes: ('name', 'args', 'returns') |
|
1140 self.__visitDefinition(node) |
|
1141 |
|
1142 def visit_AsyncFunctionDef(self, node): |
|
1143 """ |
|
1144 Public method to handle an asynchronous function definition. |
|
1145 |
|
1146 @param node reference to the node to handle |
|
1147 @type ast.AsyncFunctionDef |
|
1148 """ |
|
1149 # Skipped nodes: ('name', 'args', 'returns') |
|
1150 self.__visitDefinition(node) |
|
1151 |
|
1152 def visit_Call(self, node): |
|
1153 """ |
|
1154 Public method to handle a function call. |
|
1155 |
|
1156 @param node reference to the node to handle |
|
1157 @type ast.Call |
|
1158 """ |
|
1159 if ( |
|
1160 isinstance(node.func, ast.Attribute) and |
|
1161 node.func.attr == 'format' |
|
1162 ): |
|
1163 if AstUtilities.isBaseString(node.func.value): |
|
1164 self.calls[node.func.value] = (node, False) |
|
1165 elif ( |
|
1166 isinstance(node.func.value, ast.Name) and |
|
1167 node.func.value.id == 'str' and |
|
1168 node.args and |
|
1169 AstUtilities.isBaseString(node.args[0]) |
|
1170 ): |
|
1171 self.calls[node.args[0]] = (node, True) |
|
1172 super().generic_visit(node) |
|
1173 |
|
1174 |
|
1175 class LoggingVisitor(ast.NodeVisitor): |
|
1176 """ |
|
1177 Class implementing a node visitor to check logging statements. |
|
1178 """ |
|
1179 LoggingLevels = { |
|
1180 "debug", |
|
1181 "critical", |
|
1182 "error", |
|
1183 "info", |
|
1184 "warn", |
|
1185 "warning", |
|
1186 } |
|
1187 |
|
1188 def __init__(self): |
|
1189 """ |
|
1190 Constructor |
|
1191 """ |
|
1192 super().__init__() |
|
1193 |
|
1194 self.__currentLoggingCall = None |
|
1195 self.__currentLoggingArgument = None |
|
1196 self.__currentLoggingLevel = None |
|
1197 self.__currentExtraKeyword = None |
|
1198 self.violations = [] |
|
1199 |
|
1200 def __withinLoggingStatement(self): |
|
1201 """ |
|
1202 Private method to check, if we are inside a logging statement. |
|
1203 |
|
1204 @return flag indicating we are inside a logging statement |
|
1205 @rtype bool |
|
1206 """ |
|
1207 return self.__currentLoggingCall is not None |
|
1208 |
|
1209 def __withinLoggingArgument(self): |
|
1210 """ |
|
1211 Private method to check, if we are inside a logging argument. |
|
1212 |
|
1213 @return flag indicating we are inside a logging argument |
|
1214 @rtype bool |
|
1215 """ |
|
1216 return self.__currentLoggingArgument is not None |
|
1217 |
|
1218 def __withinExtraKeyword(self, node): |
|
1219 """ |
|
1220 Private method to check, if we are inside the extra keyword. |
|
1221 |
|
1222 @param node reference to the node to be checked |
|
1223 @type ast.keyword |
|
1224 @return flag indicating we are inside the extra keyword |
|
1225 @rtype bool |
|
1226 """ |
|
1227 return ( |
|
1228 self.__currentExtraKeyword is not None and |
|
1229 self.__currentExtraKeyword != node |
|
1230 ) |
|
1231 |
|
1232 def __detectLoggingLevel(self, node): |
|
1233 """ |
|
1234 Private method to decide whether an AST Call is a logging call. |
|
1235 |
|
1236 @param node reference to the node to be processed |
|
1237 @type ast.Call |
|
1238 @return logging level |
|
1239 @rtype str or None |
|
1240 """ |
|
1241 with contextlib.suppress(AttributeError): |
|
1242 if node.func.value.id == "warnings": |
|
1243 return None |
|
1244 |
|
1245 if node.func.attr in LoggingVisitor.LoggingLevels: |
|
1246 return node.func.attr |
|
1247 |
|
1248 return None |
|
1249 |
|
1250 def __isFormatCall(self, node): |
|
1251 """ |
|
1252 Private method to check if a function call uses format. |
|
1253 |
|
1254 @param node reference to the node to be processed |
|
1255 @type ast.Call |
|
1256 @return flag indicating the function call uses format |
|
1257 @rtype bool |
|
1258 """ |
|
1259 try: |
|
1260 return node.func.attr == "format" |
|
1261 except AttributeError: |
|
1262 return False |
|
1263 |
|
1264 def visit_Call(self, node): |
|
1265 """ |
|
1266 Public method to handle a function call. |
|
1267 |
|
1268 Every logging statement and string format is expected to be a function |
|
1269 call. |
|
1270 |
|
1271 @param node reference to the node to be processed |
|
1272 @type ast.Call |
|
1273 """ |
|
1274 # we are in a logging statement |
|
1275 if ( |
|
1276 self.__withinLoggingStatement() and |
|
1277 self.__withinLoggingArgument() and |
|
1278 self.__isFormatCall(node) |
|
1279 ): |
|
1280 self.violations.append((node, "M651")) |
|
1281 super().generic_visit(node) |
|
1282 return |
|
1283 |
|
1284 loggingLevel = self.__detectLoggingLevel(node) |
|
1285 |
|
1286 if loggingLevel and self.__currentLoggingLevel is None: |
|
1287 self.__currentLoggingLevel = loggingLevel |
|
1288 |
|
1289 # we are in some other statement |
|
1290 if loggingLevel is None: |
|
1291 super().generic_visit(node) |
|
1292 return |
|
1293 |
|
1294 # we are entering a new logging statement |
|
1295 self.__currentLoggingCall = node |
|
1296 |
|
1297 if loggingLevel == "warn": |
|
1298 self.violations.append((node, "M655")) |
|
1299 |
|
1300 for index, child in enumerate(ast.iter_child_nodes(node)): |
|
1301 if index == 1: |
|
1302 self.__currentLoggingArgument = child |
|
1303 if ( |
|
1304 index > 1 and |
|
1305 isinstance(child, ast.keyword) and |
|
1306 child.arg == "extra" |
|
1307 ): |
|
1308 self.__currentExtraKeyword = child |
|
1309 |
|
1310 super().visit(child) |
|
1311 |
|
1312 self.__currentLoggingArgument = None |
|
1313 self.__currentExtraKeyword = None |
|
1314 |
|
1315 self.__currentLoggingCall = None |
|
1316 self.__currentLoggingLevel = None |
|
1317 |
|
1318 def visit_BinOp(self, node): |
|
1319 """ |
|
1320 Public method to handle binary operations while processing the first |
|
1321 logging argument. |
|
1322 |
|
1323 @param node reference to the node to be processed |
|
1324 @type ast.BinOp |
|
1325 """ |
|
1326 if self.__withinLoggingStatement() and self.__withinLoggingArgument(): |
|
1327 # handle percent format |
|
1328 if isinstance(node.op, ast.Mod): |
|
1329 self.violations.append((node, "M652")) |
|
1330 |
|
1331 # handle string concat |
|
1332 if isinstance(node.op, ast.Add): |
|
1333 self.violations.append((node, "M653")) |
|
1334 |
|
1335 super().generic_visit(node) |
|
1336 |
|
1337 def visit_JoinedStr(self, node): |
|
1338 """ |
|
1339 Public method to handle f-string arguments. |
|
1340 |
|
1341 @param node reference to the node to be processed |
|
1342 @type ast.JoinedStr |
|
1343 """ |
|
1344 if ( |
|
1345 self.__withinLoggingStatement() and |
|
1346 any(isinstance(i, ast.FormattedValue) for i in node.values) and |
|
1347 self.__withinLoggingArgument() |
|
1348 ): |
|
1349 self.violations.append((node, "M654")) |
|
1350 |
|
1351 super().generic_visit(node) |
|
1352 |
|
1353 |
|
1354 class BugBearVisitor(ast.NodeVisitor): |
|
1355 """ |
|
1356 Class implementing a node visitor to check for various topics. |
|
1357 """ |
|
1358 # |
|
1359 # This class was implemented along the BugBear flake8 extension (v 19.3.0). |
|
1360 # Original: Copyright (c) 2016 Łukasz Langa |
|
1361 # |
|
1362 |
|
1363 NodeWindowSize = 4 |
|
1364 |
|
1365 def __init__(self): |
|
1366 """ |
|
1367 Constructor |
|
1368 """ |
|
1369 super().__init__() |
|
1370 |
|
1371 self.__nodeStack = [] |
|
1372 self.__nodeWindow = [] |
|
1373 self.violations = [] |
|
1374 |
|
1375 def visit(self, node): |
|
1376 """ |
|
1377 Public method to traverse a given AST node. |
|
1378 |
|
1379 @param node AST node to be traversed |
|
1380 @type ast.Node |
|
1381 """ |
|
1382 self.__nodeStack.append(node) |
|
1383 self.__nodeWindow.append(node) |
|
1384 self.__nodeWindow = self.__nodeWindow[-BugBearVisitor.NodeWindowSize:] |
|
1385 |
|
1386 super().visit(node) |
|
1387 |
|
1388 self.__nodeStack.pop() |
|
1389 |
|
1390 def visit_UAdd(self, node): |
|
1391 """ |
|
1392 Public method to handle unary additions. |
|
1393 |
|
1394 @param node reference to the node to be processed |
|
1395 @type ast.UAdd |
|
1396 """ |
|
1397 trailingNodes = list(map(type, self.__nodeWindow[-4:])) |
|
1398 if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]: |
|
1399 originator = self.__nodeWindow[-4] |
|
1400 self.violations.append((originator, "M501")) |
|
1401 |
|
1402 self.generic_visit(node) |
|
1403 |
|
1404 def visit_Call(self, node): |
|
1405 """ |
|
1406 Public method to handle a function call. |
|
1407 |
|
1408 @param node reference to the node to be processed |
|
1409 @type ast.Call |
|
1410 """ |
|
1411 validPaths = ("six", "future.utils", "builtins") |
|
1412 methodsDict = { |
|
1413 "M521": ("iterkeys", "itervalues", "iteritems", "iterlists"), |
|
1414 "M522": ("viewkeys", "viewvalues", "viewitems", "viewlists"), |
|
1415 "M523": ("next",), |
|
1416 } |
|
1417 |
|
1418 if isinstance(node.func, ast.Attribute): |
|
1419 for code, methods in methodsDict.items(): |
|
1420 if node.func.attr in methods: |
|
1421 callPath = ".".join(composeCallPath(node.func.value)) |
|
1422 if callPath not in validPaths: |
|
1423 self.violations.append((node, code)) |
|
1424 break |
|
1425 else: |
|
1426 self.__checkForM502(node) |
|
1427 else: |
|
1428 with contextlib.suppress(AttributeError, IndexError): |
|
1429 # bad super() call |
|
1430 if isinstance(node.func, ast.Name) and node.func.id == "super": |
|
1431 args = node.args |
|
1432 if ( |
|
1433 len(args) == 2 and |
|
1434 isinstance(args[0], ast.Attribute) and |
|
1435 isinstance(args[0].value, ast.Name) and |
|
1436 args[0].value.id == 'self' and |
|
1437 args[0].attr == '__class__' |
|
1438 ): |
|
1439 self.violations.append((node, "M509")) |
|
1440 |
|
1441 # bad getattr and setattr |
|
1442 if ( |
|
1443 node.func.id in ("getattr", "hasattr") and |
|
1444 node.args[1].s == "__call__" |
|
1445 ): |
|
1446 self.violations.append((node, "M511")) |
|
1447 if ( |
|
1448 node.func.id == "getattr" and |
|
1449 len(node.args) == 2 and |
|
1450 AstUtilities.isString(node.args[1]) |
|
1451 ): |
|
1452 self.violations.append((node, "M512")) |
|
1453 elif ( |
|
1454 node.func.id == "setattr" and |
|
1455 len(node.args) == 3 and |
|
1456 AstUtilities.isString(node.args[1]) |
|
1457 ): |
|
1458 self.violations.append((node, "M513")) |
|
1459 |
|
1460 self.generic_visit(node) |
|
1461 |
|
1462 def visit_Attribute(self, node): |
|
1463 """ |
|
1464 Public method to handle attributes. |
|
1465 |
|
1466 @param node reference to the node to be processed |
|
1467 @type ast.Attribute |
|
1468 """ |
|
1469 callPath = list(composeCallPath(node)) |
|
1470 |
|
1471 if '.'.join(callPath) == 'sys.maxint': |
|
1472 self.violations.append((node, "M504")) |
|
1473 |
|
1474 elif ( |
|
1475 len(callPath) == 2 and |
|
1476 callPath[1] == 'message' |
|
1477 ): |
|
1478 name = callPath[0] |
|
1479 for elem in reversed(self.__nodeStack[:-1]): |
|
1480 if isinstance(elem, ast.ExceptHandler) and elem.name == name: |
|
1481 self.violations.append((node, "M505")) |
|
1482 break |
|
1483 |
|
1484 def visit_Assign(self, node): |
|
1485 """ |
|
1486 Public method to handle assignments. |
|
1487 |
|
1488 @param node reference to the node to be processed |
|
1489 @type ast.Assign |
|
1490 """ |
|
1491 if isinstance(self.__nodeStack[-2], ast.ClassDef): |
|
1492 # By using 'hasattr' below we're ignoring starred arguments, slices |
|
1493 # and tuples for simplicity. |
|
1494 assignTargets = {t.id for t in node.targets if hasattr(t, 'id')} |
|
1495 if '__metaclass__' in assignTargets: |
|
1496 self.violations.append((node, "M524")) |
|
1497 |
|
1498 elif len(node.targets) == 1: |
|
1499 target = node.targets[0] |
|
1500 if ( |
|
1501 isinstance(target, ast.Attribute) and |
|
1502 isinstance(target.value, ast.Name) and |
|
1503 (target.value.id, target.attr) == ('os', 'environ') |
|
1504 ): |
|
1505 self.violations.append((node, "M506")) |
|
1506 |
|
1507 self.generic_visit(node) |
|
1508 |
|
1509 def visit_For(self, node): |
|
1510 """ |
|
1511 Public method to handle 'for' statements. |
|
1512 |
|
1513 @param node reference to the node to be processed |
|
1514 @type ast.For |
|
1515 """ |
|
1516 self.__checkForM507(node) |
|
1517 |
|
1518 self.generic_visit(node) |
|
1519 |
|
1520 def visit_AsyncFor(self, node): |
|
1521 """ |
|
1522 Public method to handle 'for' statements. |
|
1523 |
|
1524 @param node reference to the node to be processed |
|
1525 @type ast.AsyncFor |
|
1526 """ |
|
1527 self.__checkForM507(node) |
|
1528 |
|
1529 self.generic_visit(node) |
|
1530 |
|
1531 def visit_Assert(self, node): |
|
1532 """ |
|
1533 Public method to handle 'assert' statements. |
|
1534 |
|
1535 @param node reference to the node to be processed |
|
1536 @type ast.Assert |
|
1537 """ |
|
1538 if ( |
|
1539 AstUtilities.isNameConstant(node.test) and |
|
1540 AstUtilities.getValue(node.test) is False |
|
1541 ): |
|
1542 self.violations.append((node, "M503")) |
|
1543 |
|
1544 self.generic_visit(node) |
|
1545 |
|
1546 def visit_JoinedStr(self, node): |
|
1547 """ |
|
1548 Public method to handle f-string arguments. |
|
1549 |
|
1550 @param node reference to the node to be processed |
|
1551 @type ast.JoinedStr |
|
1552 """ |
|
1553 for value in node.values: |
|
1554 if isinstance(value, ast.FormattedValue): |
|
1555 return |
|
1556 |
|
1557 self.violations.append((node, "M508")) |
|
1558 |
|
1559 def __checkForM502(self, node): |
|
1560 """ |
|
1561 Private method to check the use of *strip(). |
|
1562 |
|
1563 @param node reference to the node to be processed |
|
1564 @type ast.Call |
|
1565 """ |
|
1566 if node.func.attr not in ("lstrip", "rstrip", "strip"): |
|
1567 return # method name doesn't match |
|
1568 |
|
1569 if len(node.args) != 1 or not AstUtilities.isString(node.args[0]): |
|
1570 return # used arguments don't match the builtin strip |
|
1571 |
|
1572 s = AstUtilities.getValue(node.args[0]) |
|
1573 if len(s) == 1: |
|
1574 return # stripping just one character |
|
1575 |
|
1576 if len(s) == len(set(s)): |
|
1577 return # no characters appear more than once |
|
1578 |
|
1579 self.violations.append((node, "M502")) |
|
1580 |
|
1581 def __checkForM507(self, node): |
|
1582 """ |
|
1583 Private method to check for unused loop variables. |
|
1584 |
|
1585 @param node reference to the node to be processed |
|
1586 @type ast.For |
|
1587 """ |
|
1588 targets = NameFinder() |
|
1589 targets.visit(node.target) |
|
1590 ctrlNames = set(filter(lambda s: not s.startswith('_'), |
|
1591 targets.getNames())) |
|
1592 body = NameFinder() |
|
1593 for expr in node.body: |
|
1594 body.visit(expr) |
|
1595 usedNames = set(body.getNames()) |
|
1596 for name in sorted(ctrlNames - usedNames): |
|
1597 n = targets.getNames()[name][0] |
|
1598 self.violations.append((n, "M507", name)) |
|
1599 |
|
1600 |
|
1601 class NameFinder(ast.NodeVisitor): |
|
1602 """ |
|
1603 Class to extract a name out of a tree of nodes. |
|
1604 """ |
|
1605 def __init__(self): |
|
1606 """ |
|
1607 Constructor |
|
1608 """ |
|
1609 super().__init__() |
|
1610 |
|
1611 self.__names = {} |
|
1612 |
|
1613 def visit_Name(self, node): |
|
1614 """ |
|
1615 Public method to handle 'Name' nodes. |
|
1616 |
|
1617 @param node reference to the node to be processed |
|
1618 @type ast.Name |
|
1619 """ |
|
1620 self.__names.setdefault(node.id, []).append(node) |
|
1621 |
|
1622 def visit(self, node): |
|
1623 """ |
|
1624 Public method to traverse a given AST node. |
|
1625 |
|
1626 @param node AST node to be traversed |
|
1627 @type ast.Node |
|
1628 """ |
|
1629 if isinstance(node, list): |
|
1630 for elem in node: |
|
1631 super().visit(elem) |
|
1632 else: |
|
1633 super().visit(node) |
|
1634 |
|
1635 def getNames(self): |
|
1636 """ |
|
1637 Public method to return the extracted names and Name nodes. |
|
1638 |
|
1639 @return dictionary containing the names as keys and the list of nodes |
|
1640 @rtype dict |
|
1641 """ |
|
1642 return self.__names |
|
1643 |
|
1644 |
|
1645 class ReturnVisitor(ast.NodeVisitor): |
|
1646 """ |
|
1647 Class implementing a node visitor to check return statements. |
|
1648 """ |
|
1649 Assigns = 'assigns' |
|
1650 Refs = 'refs' |
|
1651 Returns = 'returns' |
|
1652 |
|
1653 def __init__(self): |
|
1654 """ |
|
1655 Constructor |
|
1656 """ |
|
1657 super().__init__() |
|
1658 |
|
1659 self.__stack = [] |
|
1660 self.violations = [] |
|
1661 self.__loopCount = 0 |
|
1662 |
|
1663 @property |
|
1664 def assigns(self): |
|
1665 """ |
|
1666 Public method to get the Assign nodes. |
|
1667 |
|
1668 @return dictionary containing the node name as key and line number |
|
1669 as value |
|
1670 @rtype dict |
|
1671 """ |
|
1672 return self.__stack[-1][ReturnVisitor.Assigns] |
|
1673 |
|
1674 @property |
|
1675 def refs(self): |
|
1676 """ |
|
1677 Public method to get the References nodes. |
|
1678 |
|
1679 @return dictionary containing the node name as key and line number |
|
1680 as value |
|
1681 @rtype dict |
|
1682 """ |
|
1683 return self.__stack[-1][ReturnVisitor.Refs] |
|
1684 |
|
1685 @property |
|
1686 def returns(self): |
|
1687 """ |
|
1688 Public method to get the Return nodes. |
|
1689 |
|
1690 @return dictionary containing the node name as key and line number |
|
1691 as value |
|
1692 @rtype dict |
|
1693 """ |
|
1694 return self.__stack[-1][ReturnVisitor.Returns] |
|
1695 |
|
1696 def visit_For(self, node): |
|
1697 """ |
|
1698 Public method to handle a for loop. |
|
1699 |
|
1700 @param node reference to the for node to handle |
|
1701 @type ast.For |
|
1702 """ |
|
1703 self.__visitLoop(node) |
|
1704 |
|
1705 def visit_AsyncFor(self, node): |
|
1706 """ |
|
1707 Public method to handle an async for loop. |
|
1708 |
|
1709 @param node reference to the async for node to handle |
|
1710 @type ast.AsyncFor |
|
1711 """ |
|
1712 self.__visitLoop(node) |
|
1713 |
|
1714 def visit_While(self, node): |
|
1715 """ |
|
1716 Public method to handle a while loop. |
|
1717 |
|
1718 @param node reference to the while node to handle |
|
1719 @type ast.While |
|
1720 """ |
|
1721 self.__visitLoop(node) |
|
1722 |
|
1723 def __visitLoop(self, node): |
|
1724 """ |
|
1725 Private method to handle loop nodes. |
|
1726 |
|
1727 @param node reference to the loop node to handle |
|
1728 @type ast.For, ast.AsyncFor or ast.While |
|
1729 """ |
|
1730 self.__loopCount += 1 |
|
1731 self.generic_visit(node) |
|
1732 self.__loopCount -= 1 |
|
1733 |
|
1734 def __visitWithStack(self, node): |
|
1735 """ |
|
1736 Private method to traverse a given function node using a stack. |
|
1737 |
|
1738 @param node AST node to be traversed |
|
1739 @type ast.FunctionDef or ast.AsyncFunctionDef |
|
1740 """ |
|
1741 self.__stack.append({ |
|
1742 ReturnVisitor.Assigns: defaultdict(list), |
|
1743 ReturnVisitor.Refs: defaultdict(list), |
|
1744 ReturnVisitor.Returns: [] |
|
1745 }) |
|
1746 |
|
1747 self.generic_visit(node) |
|
1748 self.__checkFunction(node) |
|
1749 self.__stack.pop() |
|
1750 |
|
1751 def visit_FunctionDef(self, node): |
|
1752 """ |
|
1753 Public method to handle a function definition. |
|
1754 |
|
1755 @param node reference to the node to handle |
|
1756 @type ast.FunctionDef |
|
1757 """ |
|
1758 self.__visitWithStack(node) |
|
1759 |
|
1760 def visit_AsyncFunctionDef(self, node): |
|
1761 """ |
|
1762 Public method to handle a function definition. |
|
1763 |
|
1764 @param node reference to the node to handle |
|
1765 @type ast.AsyncFunctionDef |
|
1766 """ |
|
1767 self.__visitWithStack(node) |
|
1768 |
|
1769 def visit_Return(self, node): |
|
1770 """ |
|
1771 Public method to handle a return node. |
|
1772 |
|
1773 @param node reference to the node to handle |
|
1774 @type ast.Return |
|
1775 """ |
|
1776 self.returns.append(node) |
|
1777 self.generic_visit(node) |
|
1778 |
|
1779 def visit_Assign(self, node): |
|
1780 """ |
|
1781 Public method to handle an assign node. |
|
1782 |
|
1783 @param node reference to the node to handle |
|
1784 @type ast.Assign |
|
1785 """ |
|
1786 if not self.__stack: |
|
1787 return |
|
1788 |
|
1789 self.generic_visit(node.value) |
|
1790 |
|
1791 target = node.targets[0] |
|
1792 if ( |
|
1793 isinstance(target, ast.Tuple) and |
|
1794 not isinstance(node.value, ast.Tuple) |
|
1795 ): |
|
1796 # skip unpacking assign |
|
1797 return |
|
1798 |
|
1799 self.__visitAssignTarget(target) |
|
1800 |
|
1801 def visit_Name(self, node): |
|
1802 """ |
|
1803 Public method to handle a name node. |
|
1804 |
|
1805 @param node reference to the node to handle |
|
1806 @type ast.Name |
|
1807 """ |
|
1808 if self.__stack: |
|
1809 self.refs[node.id].append(node.lineno) |
|
1810 |
|
1811 def __visitAssignTarget(self, node): |
|
1812 """ |
|
1813 Private method to handle an assign target node. |
|
1814 |
|
1815 @param node reference to the node to handle |
|
1816 @type ast.AST |
|
1817 """ |
|
1818 if isinstance(node, ast.Tuple): |
|
1819 for elt in node.elts: |
|
1820 self.__visitAssignTarget(elt) |
|
1821 return |
|
1822 |
|
1823 if not self.__loopCount and isinstance(node, ast.Name): |
|
1824 self.assigns[node.id].append(node.lineno) |
|
1825 return |
|
1826 |
|
1827 self.generic_visit(node) |
|
1828 |
|
1829 def __checkFunction(self, node): |
|
1830 """ |
|
1831 Private method to check a function definition node. |
|
1832 |
|
1833 @param node reference to the node to check |
|
1834 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
1835 """ |
|
1836 if not self.returns or not node.body: |
|
1837 return |
|
1838 |
|
1839 if len(node.body) == 1 and isinstance(node.body[-1], ast.Return): |
|
1840 # skip functions that consist of `return None` only |
|
1841 return |
|
1842 |
|
1843 if not self.__resultExists(): |
|
1844 self.__checkUnnecessaryReturnNone() |
|
1845 return |
|
1846 |
|
1847 self.__checkImplicitReturnValue() |
|
1848 self.__checkImplicitReturn(node.body[-1]) |
|
1849 |
|
1850 for n in self.returns: |
|
1851 if n.value: |
|
1852 self.__checkUnnecessaryAssign(n.value) |
|
1853 |
|
1854 def __isNone(self, node): |
|
1855 """ |
|
1856 Private method to check, if a node value is None. |
|
1857 |
|
1858 @param node reference to the node to check |
|
1859 @type ast.AST |
|
1860 @return flag indicating the node contains a None value |
|
1861 @rtype bool |
|
1862 """ |
|
1863 return ( |
|
1864 AstUtilities.isNameConstant(node) and |
|
1865 AstUtilities.getValue(node) is None |
|
1866 ) |
|
1867 |
|
1868 def __isFalse(self, node): |
|
1869 """ |
|
1870 Private method to check, if a node value is False. |
|
1871 |
|
1872 @param node reference to the node to check |
|
1873 @type ast.AST |
|
1874 @return flag indicating the node contains a False value |
|
1875 @rtype bool |
|
1876 """ |
|
1877 return ( |
|
1878 AstUtilities.isNameConstant(node) and |
|
1879 AstUtilities.getValue(node) is False |
|
1880 ) |
|
1881 |
|
1882 def __resultExists(self): |
|
1883 """ |
|
1884 Private method to check the existance of a return result. |
|
1885 |
|
1886 @return flag indicating the existence of a return result |
|
1887 @rtype bool |
|
1888 """ |
|
1889 for node in self.returns: |
|
1890 value = node.value |
|
1891 if value and not self.__isNone(value): |
|
1892 return True |
|
1893 |
|
1894 return False |
|
1895 |
|
1896 def __checkImplicitReturnValue(self): |
|
1897 """ |
|
1898 Private method to check for implicit return values. |
|
1899 """ |
|
1900 for node in self.returns: |
|
1901 if not node.value: |
|
1902 self.violations.append((node, "M832")) |
|
1903 |
|
1904 def __checkUnnecessaryReturnNone(self): |
|
1905 """ |
|
1906 Private method to check for an unnecessary 'return None' statement. |
|
1907 """ |
|
1908 for node in self.returns: |
|
1909 if self.__isNone(node.value): |
|
1910 self.violations.append((node, "M831")) |
|
1911 |
|
1912 def __checkImplicitReturn(self, node): |
|
1913 """ |
|
1914 Private method to check for an implicit return statement. |
|
1915 |
|
1916 @param node reference to the node to check |
|
1917 @type ast.AST |
|
1918 """ |
|
1919 if isinstance(node, ast.If): |
|
1920 if not node.body or not node.orelse: |
|
1921 self.violations.append((node, "M833")) |
|
1922 return |
|
1923 |
|
1924 self.__checkImplicitReturn(node.body[-1]) |
|
1925 self.__checkImplicitReturn(node.orelse[-1]) |
|
1926 return |
|
1927 |
|
1928 if isinstance(node, (ast.For, ast.AsyncFor)) and node.orelse: |
|
1929 self.__checkImplicitReturn(node.orelse[-1]) |
|
1930 return |
|
1931 |
|
1932 if isinstance(node, (ast.With, ast.AsyncWith)): |
|
1933 self.__checkImplicitReturn(node.body[-1]) |
|
1934 return |
|
1935 |
|
1936 if isinstance(node, ast.Assert) and self.__isFalse(node.test): |
|
1937 return |
|
1938 |
|
1939 try: |
|
1940 okNodes = (ast.Return, ast.Raise, ast.While, ast.Try) |
|
1941 except AttributeError: |
|
1942 okNodes = (ast.Return, ast.Raise, ast.While) |
|
1943 if not isinstance(node, okNodes): |
|
1944 self.violations.append((node, "M833")) |
|
1945 |
|
1946 def __checkUnnecessaryAssign(self, node): |
|
1947 """ |
|
1948 Private method to check for an unnecessary assign statement. |
|
1949 |
|
1950 @param node reference to the node to check |
|
1951 @type ast.AST |
|
1952 """ |
|
1953 if not isinstance(node, ast.Name): |
|
1954 return |
|
1955 |
|
1956 varname = node.id |
|
1957 returnLineno = node.lineno |
|
1958 |
|
1959 if varname not in self.assigns: |
|
1960 return |
|
1961 |
|
1962 if varname not in self.refs: |
|
1963 self.violations.append((node, "M834")) |
|
1964 return |
|
1965 |
|
1966 if self.__hasRefsBeforeNextAssign(varname, returnLineno): |
|
1967 return |
|
1968 |
|
1969 self.violations.append((node, "M834")) |
|
1970 |
|
1971 def __hasRefsBeforeNextAssign(self, varname, returnLineno): |
|
1972 """ |
|
1973 Private method to check for references before a following assign |
|
1974 statement. |
|
1975 |
|
1976 @param varname variable name to check for |
|
1977 @type str |
|
1978 @param returnLineno line number of the return statement |
|
1979 @type int |
|
1980 @return flag indicating the existence of references |
|
1981 @rtype bool |
|
1982 """ |
|
1983 beforeAssign = 0 |
|
1984 afterAssign = None |
|
1985 |
|
1986 for lineno in sorted(self.assigns[varname]): |
|
1987 if lineno > returnLineno: |
|
1988 afterAssign = lineno |
|
1989 break |
|
1990 |
|
1991 if lineno <= returnLineno: |
|
1992 beforeAssign = lineno |
|
1993 |
|
1994 for lineno in self.refs[varname]: |
|
1995 if lineno == returnLineno: |
|
1996 continue |
|
1997 |
|
1998 if afterAssign: |
|
1999 if beforeAssign < lineno <= afterAssign: |
|
2000 return True |
|
2001 |
|
2002 elif beforeAssign < lineno: |
|
2003 return True |
|
2004 |
|
2005 return False |
|
2006 |
|
2007 |
|
2008 class DateTimeVisitor(ast.NodeVisitor): |
|
2009 """ |
|
2010 Class implementing a node visitor to check datetime function calls. |
|
2011 |
|
2012 Note: This class is modelled after flake8_datetimez checker. |
|
2013 """ |
|
2014 def __init__(self): |
|
2015 """ |
|
2016 Constructor |
|
2017 """ |
|
2018 super().__init__() |
|
2019 |
|
2020 self.violations = [] |
|
2021 |
|
2022 def __getFromKeywords(self, keywords, name): |
|
2023 """ |
|
2024 Private method to get a keyword node given its name. |
|
2025 |
|
2026 @param keywords list of keyword argument nodes |
|
2027 @type list of ast.AST |
|
2028 @param name name of the keyword node |
|
2029 @type str |
|
2030 @return keyword node |
|
2031 @rtype ast.AST |
|
2032 """ |
|
2033 for keyword in keywords: |
|
2034 if keyword.arg == name: |
|
2035 return keyword |
|
2036 |
|
2037 return None |
|
2038 |
|
2039 def visit_Call(self, node): |
|
2040 """ |
|
2041 Public method to handle a function call. |
|
2042 |
|
2043 Every datetime related function call is check for use of the naive |
|
2044 variant (i.e. use without TZ info). |
|
2045 |
|
2046 @param node reference to the node to be processed |
|
2047 @type ast.Call |
|
2048 """ |
|
2049 # datetime.something() |
|
2050 isDateTimeClass = ( |
|
2051 isinstance(node.func, ast.Attribute) and |
|
2052 isinstance(node.func.value, ast.Name) and |
|
2053 node.func.value.id == 'datetime') |
|
2054 |
|
2055 # datetime.datetime.something() |
|
2056 isDateTimeModuleAndClass = ( |
|
2057 isinstance(node.func, ast.Attribute) and |
|
2058 isinstance(node.func.value, ast.Attribute) and |
|
2059 node.func.value.attr == 'datetime' and |
|
2060 isinstance(node.func.value.value, ast.Name) and |
|
2061 node.func.value.value.id == 'datetime') |
|
2062 |
|
2063 if isDateTimeClass: |
|
2064 if node.func.attr == 'datetime': |
|
2065 # datetime.datetime(2000, 1, 1, 0, 0, 0, 0, |
|
2066 # datetime.timezone.utc) |
|
2067 isCase1 = ( |
|
2068 len(node.args) >= 8 and |
|
2069 not ( |
|
2070 AstUtilities.isNameConstant(node.args[7]) and |
|
2071 AstUtilities.getValue(node.args[7]) is None |
|
2072 ) |
|
2073 ) |
|
2074 |
|
2075 # datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc) |
|
2076 tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo') |
|
2077 isCase2 = ( |
|
2078 tzinfoKeyword is not None and |
|
2079 not ( |
|
2080 AstUtilities.isNameConstant(tzinfoKeyword.value) and |
|
2081 AstUtilities.getValue(tzinfoKeyword.value) is None |
|
2082 ) |
|
2083 ) |
|
2084 |
|
2085 if not (isCase1 or isCase2): |
|
2086 self.violations.append((node, "M301")) |
|
2087 |
|
2088 elif node.func.attr == 'time': |
|
2089 # time(12, 10, 45, 0, datetime.timezone.utc) |
|
2090 isCase1 = ( |
|
2091 len(node.args) >= 5 and |
|
2092 not ( |
|
2093 AstUtilities.isNameConstant(node.args[4]) and |
|
2094 AstUtilities.getValue(node.args[4]) is None |
|
2095 ) |
|
2096 ) |
|
2097 |
|
2098 # datetime.time(12, 10, 45, tzinfo=datetime.timezone.utc) |
|
2099 tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo') |
|
2100 isCase2 = ( |
|
2101 tzinfoKeyword is not None and |
|
2102 not ( |
|
2103 AstUtilities.isNameConstant(tzinfoKeyword.value) and |
|
2104 AstUtilities.getValue(tzinfoKeyword.value) is None |
|
2105 ) |
|
2106 ) |
|
2107 |
|
2108 if not (isCase1 or isCase2): |
|
2109 self.violations.append((node, "M321")) |
|
2110 |
|
2111 elif node.func.attr == 'date': |
|
2112 self.violations.append((node, "M311")) |
|
2113 |
|
2114 if isDateTimeClass or isDateTimeModuleAndClass: |
|
2115 if node.func.attr == 'today': |
|
2116 self.violations.append((node, "M302")) |
|
2117 |
|
2118 elif node.func.attr == 'utcnow': |
|
2119 self.violations.append((node, "M303")) |
|
2120 |
|
2121 elif node.func.attr == 'utcfromtimestamp': |
|
2122 self.violations.append((node, "M304")) |
|
2123 |
|
2124 elif node.func.attr in 'now': |
|
2125 # datetime.now(UTC) |
|
2126 isCase1 = ( |
|
2127 len(node.args) == 1 and |
|
2128 len(node.keywords) == 0 and |
|
2129 not ( |
|
2130 AstUtilities.isNameConstant(node.args[0]) and |
|
2131 AstUtilities.getValue(node.args[0]) is None |
|
2132 ) |
|
2133 ) |
|
2134 |
|
2135 # datetime.now(tz=UTC) |
|
2136 tzKeyword = self.__getFromKeywords(node.keywords, 'tz') |
|
2137 isCase2 = ( |
|
2138 tzKeyword is not None and |
|
2139 not ( |
|
2140 AstUtilities.isNameConstant(tzKeyword.value) and |
|
2141 AstUtilities.getValue(tzKeyword.value) is None |
|
2142 ) |
|
2143 ) |
|
2144 |
|
2145 if not (isCase1 or isCase2): |
|
2146 self.violations.append((node, "M305")) |
|
2147 |
|
2148 elif node.func.attr == 'fromtimestamp': |
|
2149 # datetime.fromtimestamp(1234, UTC) |
|
2150 isCase1 = ( |
|
2151 len(node.args) == 2 and |
|
2152 len(node.keywords) == 0 and |
|
2153 not ( |
|
2154 AstUtilities.isNameConstant(node.args[1]) and |
|
2155 AstUtilities.getValue(node.args[1]) is None |
|
2156 ) |
|
2157 ) |
|
2158 |
|
2159 # datetime.fromtimestamp(1234, tz=UTC) |
|
2160 tzKeyword = self.__getFromKeywords(node.keywords, 'tz') |
|
2161 isCase2 = ( |
|
2162 tzKeyword is not None and |
|
2163 not ( |
|
2164 AstUtilities.isNameConstant(tzKeyword.value) and |
|
2165 AstUtilities.getValue(tzKeyword.value) is None |
|
2166 ) |
|
2167 ) |
|
2168 |
|
2169 if not (isCase1 or isCase2): |
|
2170 self.violations.append((node, "M306")) |
|
2171 |
|
2172 elif node.func.attr == 'strptime': |
|
2173 # datetime.strptime(...).replace(tzinfo=UTC) |
|
2174 parent = getattr(node, '_dtCheckerParent', None) |
|
2175 pparent = getattr(parent, '_dtCheckerParent', None) |
|
2176 if ( |
|
2177 not (isinstance(parent, ast.Attribute) and |
|
2178 parent.attr == 'replace') or |
|
2179 not isinstance(pparent, ast.Call) |
|
2180 ): |
|
2181 isCase1 = False |
|
2182 else: |
|
2183 tzinfoKeyword = self.__getFromKeywords(pparent.keywords, |
|
2184 'tzinfo') |
|
2185 isCase1 = ( |
|
2186 tzinfoKeyword is not None and |
|
2187 not ( |
|
2188 AstUtilities.isNameConstant( |
|
2189 tzinfoKeyword.value) and |
|
2190 AstUtilities.getValue(tzinfoKeyword.value) is None |
|
2191 ) |
|
2192 ) |
|
2193 |
|
2194 if not isCase1: |
|
2195 self.violations.append((node, "M307")) |
|
2196 |
|
2197 elif node.func.attr == 'fromordinal': |
|
2198 self.violations.append((node, "M308")) |
|
2199 |
|
2200 # date.something() |
|
2201 isDateClass = (isinstance(node.func, ast.Attribute) and |
|
2202 isinstance(node.func.value, ast.Name) and |
|
2203 node.func.value.id == 'date') |
|
2204 |
|
2205 # datetime.date.something() |
|
2206 isDateModuleAndClass = (isinstance(node.func, ast.Attribute) and |
|
2207 isinstance(node.func.value, ast.Attribute) and |
|
2208 node.func.value.attr == 'date' and |
|
2209 isinstance(node.func.value.value, ast.Name) and |
|
2210 node.func.value.value.id == 'datetime') |
|
2211 |
|
2212 if isDateClass or isDateModuleAndClass: |
|
2213 if node.func.attr == 'today': |
|
2214 self.violations.append((node, "M312")) |
|
2215 |
|
2216 elif node.func.attr == 'fromtimestamp': |
|
2217 self.violations.append((node, "M313")) |
|
2218 |
|
2219 elif node.func.attr == 'fromordinal': |
|
2220 self.violations.append((node, "M314")) |
|
2221 |
|
2222 elif node.func.attr == 'fromisoformat': |
|
2223 self.violations.append((node, "M315")) |
|
2224 |
|
2225 self.generic_visit(node) |
|
2226 |
|
2227 |
|
2228 class SysVersionVisitor(ast.NodeVisitor): |
|
2229 """ |
|
2230 Class implementing a node visitor to check the use of sys.version and |
|
2231 sys.version_info. |
|
2232 |
|
2233 Note: This class is modelled after flake8-2020 checker. |
|
2234 """ |
|
2235 def __init__(self): |
|
2236 """ |
|
2237 Constructor |
|
2238 """ |
|
2239 super().__init__() |
|
2240 |
|
2241 self.violations = [] |
|
2242 self.__fromImports = {} |
|
2243 |
|
2244 def visit_ImportFrom(self, node): |
|
2245 """ |
|
2246 Public method to handle a from ... import ... statement. |
|
2247 |
|
2248 @param node reference to the node to be processed |
|
2249 @type ast.ImportFrom |
|
2250 """ |
|
2251 for alias in node.names: |
|
2252 if node.module is not None and not alias.asname: |
|
2253 self.__fromImports[alias.name] = node.module |
|
2254 |
|
2255 self.generic_visit(node) |
|
2256 |
|
2257 def __isSys(self, attr, node): |
|
2258 """ |
|
2259 Private method to check for a reference to sys attribute. |
|
2260 |
|
2261 @param attr attribute name |
|
2262 @type str |
|
2263 @param node reference to the node to be checked |
|
2264 @type ast.Node |
|
2265 @return flag indicating a match |
|
2266 @rtype bool |
|
2267 """ |
|
2268 match = False |
|
2269 if ( |
|
2270 (isinstance(node, ast.Attribute) and |
|
2271 isinstance(node.value, ast.Name) and |
|
2272 node.value.id == "sys" and |
|
2273 node.attr == attr) or |
|
2274 (isinstance(node, ast.Name) and |
|
2275 node.id == attr and |
|
2276 self.__fromImports.get(node.id) == "sys") |
|
2277 ): |
|
2278 match = True |
|
2279 |
|
2280 return match |
|
2281 |
|
2282 def __isSysVersionUpperSlice(self, node, n): |
|
2283 """ |
|
2284 Private method to check the upper slice of sys.version. |
|
2285 |
|
2286 @param node reference to the node to be checked |
|
2287 @type ast.Node |
|
2288 @param n slice value to check against |
|
2289 @type int |
|
2290 @return flag indicating a match |
|
2291 @rtype bool |
|
2292 """ |
|
2293 return ( |
|
2294 self.__isSys("version", node.value) and |
|
2295 isinstance(node.slice, ast.Slice) and |
|
2296 node.slice.lower is None and |
|
2297 AstUtilities.isNumber(node.slice.upper) and |
|
2298 AstUtilities.getValue(node.slice.upper) == n and |
|
2299 node.slice.step is None |
|
2300 ) |
|
2301 |
|
2302 def visit_Subscript(self, node): |
|
2303 """ |
|
2304 Public method to handle a subscript. |
|
2305 |
|
2306 @param node reference to the node to be processed |
|
2307 @type ast.Subscript |
|
2308 """ |
|
2309 if self.__isSysVersionUpperSlice(node, 1): |
|
2310 self.violations.append((node.value, "M423")) |
|
2311 elif self.__isSysVersionUpperSlice(node, 3): |
|
2312 self.violations.append((node.value, "M401")) |
|
2313 elif ( |
|
2314 self.__isSys('version', node.value) and |
|
2315 isinstance(node.slice, ast.Index) and |
|
2316 AstUtilities.isNumber(node.slice.value) and |
|
2317 AstUtilities.getValue(node.slice.value) == 2 |
|
2318 ): |
|
2319 self.violations.append((node.value, "M402")) |
|
2320 elif ( |
|
2321 self.__isSys('version', node.value) and |
|
2322 isinstance(node.slice, ast.Index) and |
|
2323 AstUtilities.isNumber(node.slice.value) and |
|
2324 AstUtilities.getValue(node.slice.value) == 0 |
|
2325 ): |
|
2326 self.violations.append((node.value, "M421")) |
|
2327 |
|
2328 self.generic_visit(node) |
|
2329 |
|
2330 def visit_Compare(self, node): |
|
2331 """ |
|
2332 Public method to handle a comparison. |
|
2333 |
|
2334 @param node reference to the node to be processed |
|
2335 @type ast.Compare |
|
2336 """ |
|
2337 if ( |
|
2338 isinstance(node.left, ast.Subscript) and |
|
2339 self.__isSys('version_info', node.left.value) and |
|
2340 isinstance(node.left.slice, ast.Index) and |
|
2341 AstUtilities.isNumber(node.left.slice.value) and |
|
2342 AstUtilities.getValue(node.left.slice.value) == 0 and |
|
2343 len(node.ops) == 1 and |
|
2344 isinstance(node.ops[0], ast.Eq) and |
|
2345 AstUtilities.isNumber(node.comparators[0]) and |
|
2346 AstUtilities.getValue(node.comparators[0]) == 3 |
|
2347 ): |
|
2348 self.violations.append((node.left, "M411")) |
|
2349 elif ( |
|
2350 self.__isSys('version', node.left) and |
|
2351 len(node.ops) == 1 and |
|
2352 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and |
|
2353 AstUtilities.isString(node.comparators[0]) |
|
2354 ): |
|
2355 if len(AstUtilities.getValue(node.comparators[0])) == 1: |
|
2356 errorCode = "M422" |
|
2357 else: |
|
2358 errorCode = "M403" |
|
2359 self.violations.append((node.left, errorCode)) |
|
2360 elif ( |
|
2361 isinstance(node.left, ast.Subscript) and |
|
2362 self.__isSys('version_info', node.left.value) and |
|
2363 isinstance(node.left.slice, ast.Index) and |
|
2364 AstUtilities.isNumber(node.left.slice.value) and |
|
2365 AstUtilities.getValue(node.left.slice.value) == 1 and |
|
2366 len(node.ops) == 1 and |
|
2367 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and |
|
2368 AstUtilities.isNumber(node.comparators[0]) |
|
2369 ): |
|
2370 self.violations.append((node, "M413")) |
|
2371 elif ( |
|
2372 isinstance(node.left, ast.Attribute) and |
|
2373 self.__isSys('version_info', node.left.value) and |
|
2374 node.left.attr == 'minor' and |
|
2375 len(node.ops) == 1 and |
|
2376 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and |
|
2377 AstUtilities.isNumber(node.comparators[0]) |
|
2378 ): |
|
2379 self.violations.append((node, "M414")) |
|
2380 |
|
2381 self.generic_visit(node) |
|
2382 |
|
2383 def visit_Attribute(self, node): |
|
2384 """ |
|
2385 Public method to handle an attribute. |
|
2386 |
|
2387 @param node reference to the node to be processed |
|
2388 @type ast.Attribute |
|
2389 """ |
|
2390 if ( |
|
2391 isinstance(node.value, ast.Name) and |
|
2392 node.value.id == 'six' and |
|
2393 node.attr == 'PY3' |
|
2394 ): |
|
2395 self.violations.append((node, "M412")) |
|
2396 |
|
2397 self.generic_visit(node) |
|
2398 |
|
2399 def visit_Name(self, node): |
|
2400 """ |
|
2401 Public method to handle an name. |
|
2402 |
|
2403 @param node reference to the node to be processed |
|
2404 @type ast.Name |
|
2405 """ |
|
2406 if node.id == 'PY3' and self.__fromImports.get(node.id) == 'six': |
|
2407 self.violations.append((node, "M412")) |
|
2408 |
|
2409 self.generic_visit(node) |
|
2410 |
|
2411 # |
|
2412 # eflag: noqa = M891 |