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