|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2015 - 2021 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", |
|
59 "M185", "M186", "M187", |
|
60 "M191", "M192", "M193", |
|
61 "M195", "M196", "M197", "M198", |
|
62 |
|
63 ## Dictionaries with sorted keys |
|
64 "M201", |
|
65 |
|
66 ## Naive datetime usage |
|
67 "M301", "M302", "M303", "M304", "M305", "M306", "M307", "M308", |
|
68 "M311", "M312", "M313", "M314", "M315", |
|
69 "M321", |
|
70 |
|
71 ## sys.version and sys.version_info usage |
|
72 "M401", "M402", "M403", |
|
73 "M411", "M412", "M413", "M414", |
|
74 "M421", "M422", "M423", |
|
75 |
|
76 ## Bugbear |
|
77 "M501", "M502", "M503", "M504", "M505", "M506", "M507", "M508", |
|
78 "M509", |
|
79 "M511", "M512", "M513", |
|
80 "M521", "M522", "M523", "M524", |
|
81 |
|
82 ## Format Strings |
|
83 "M601", |
|
84 "M611", "M612", "M613", |
|
85 "M621", "M622", "M623", "M624", "M625", |
|
86 "M631", "M632", |
|
87 |
|
88 ## Logging |
|
89 "M651", "M652", "M653", "M654", "M655", |
|
90 |
|
91 ## Future statements |
|
92 "M701", "M702", |
|
93 |
|
94 ## Gettext |
|
95 "M711", |
|
96 |
|
97 ## print |
|
98 "M801", |
|
99 |
|
100 ## one element tuple |
|
101 "M811", |
|
102 |
|
103 ## Mutable Defaults |
|
104 "M821", "M822", |
|
105 |
|
106 ## return statements |
|
107 "M831", "M832", "M833", "M834", |
|
108 |
|
109 ## line continuation |
|
110 "M841", |
|
111 |
|
112 ## commented code |
|
113 "M891", |
|
114 ] |
|
115 |
|
116 Formatter = Formatter() |
|
117 FormatFieldRegex = re.compile(r'^((?:\s|.)*?)(\..*|\[.*\])?$') |
|
118 |
|
119 BuiltinsWhiteList = [ |
|
120 "__name__", |
|
121 "__doc__", |
|
122 "credits", |
|
123 ] |
|
124 |
|
125 def __init__(self, source, filename, tree, select, ignore, expected, |
|
126 repeat, args): |
|
127 """ |
|
128 Constructor |
|
129 |
|
130 @param source source code to be checked |
|
131 @type list of str |
|
132 @param filename name of the source file |
|
133 @type str |
|
134 @param tree AST tree of the source code |
|
135 @type ast.Module |
|
136 @param select list of selected codes |
|
137 @type list of str |
|
138 @param ignore list of codes to be ignored |
|
139 @type list of str |
|
140 @param expected list of expected codes |
|
141 @type list of str |
|
142 @param repeat flag indicating to report each occurrence of a code |
|
143 @type bool |
|
144 @param args dictionary of arguments for the miscellaneous checks |
|
145 @type dict |
|
146 """ |
|
147 self.__select = tuple(select) |
|
148 self.__ignore = ('',) if select else tuple(ignore) |
|
149 self.__expected = expected[:] |
|
150 self.__repeat = repeat |
|
151 self.__filename = filename |
|
152 self.__source = source[:] |
|
153 self.__tree = copy.deepcopy(tree) |
|
154 self.__args = args |
|
155 |
|
156 self.__pep3101FormatRegex = re.compile( |
|
157 r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%') |
|
158 |
|
159 import builtins |
|
160 self.__builtins = [b for b in dir(builtins) |
|
161 if b not in self.BuiltinsWhiteList] |
|
162 |
|
163 self.__eradicator = Eradicator() |
|
164 |
|
165 # statistics counters |
|
166 self.counters = {} |
|
167 |
|
168 # collection of detected errors |
|
169 self.errors = [] |
|
170 |
|
171 checkersWithCodes = [ |
|
172 (self.__checkCoding, ("M101", "M102")), |
|
173 (self.__checkCopyright, ("M111", "M112")), |
|
174 (self.__checkBuiltins, ("M131", "M132")), |
|
175 (self.__checkComprehensions, ("M181", "M182", "M183", "M184", |
|
176 "M185", "M186", "M187", |
|
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 |
|
686 if ( |
|
687 nArgs == 1 and |
|
688 isinstance(node.args[0], ast.GeneratorExp) and |
|
689 node.func.id in ('list', 'set') |
|
690 ): |
|
691 errorCode = { |
|
692 "list": "M181", |
|
693 "set": "M182", |
|
694 }[node.func.id] |
|
695 self.__error(node.lineno - 1, node.col_offset, errorCode) |
|
696 |
|
697 elif ( |
|
698 nArgs == 1 and |
|
699 isinstance(node.args[0], ast.GeneratorExp) and |
|
700 isinstance(node.args[0].elt, ast.Tuple) and |
|
701 len(node.args[0].elt.elts) == 2 and |
|
702 node.func.id == "dict" |
|
703 ): |
|
704 self.__error(node.lineno - 1, node.col_offset, "M183") |
|
705 |
|
706 elif ( |
|
707 nArgs == 1 and |
|
708 isinstance(node.args[0], ast.ListComp) and |
|
709 node.func.id in ('list', 'set', 'dict') |
|
710 ): |
|
711 errorCode = { |
|
712 'list': 'M195', |
|
713 'dict': 'M185', |
|
714 'set': 'M184', |
|
715 }[node.func.id] |
|
716 self.__error(node.lineno - 1, node.col_offset, errorCode) |
|
717 |
|
718 elif nArgs == 1 and ( |
|
719 isinstance(node.args[0], ast.Tuple) and |
|
720 node.func.id == "tuple" or |
|
721 isinstance(node.args[0], ast.List) and |
|
722 node.func.id == "list" |
|
723 ): |
|
724 errorCode = { |
|
725 'tuple': 'M197', |
|
726 'list': 'M198', |
|
727 }[node.func.id] |
|
728 self.__error(node.lineno - 1, node.col_offset, errorCode, |
|
729 type(node.args[0]).__name__.lower(), |
|
730 node.func.id) |
|
731 |
|
732 elif ( |
|
733 nArgs == 1 and |
|
734 isinstance(node.args[0], (ast.Tuple, ast.List)) and |
|
735 node.func.id in ("tuple", "list", "set", "dict") |
|
736 ): |
|
737 errorCode = { |
|
738 "tuple": "M192", |
|
739 "list": "M193", |
|
740 "set": "M191", |
|
741 "dict": "M191", |
|
742 }[node.func.id] |
|
743 self.__error(node.lineno - 1, node.col_offset, errorCode, |
|
744 type(node.args[0]).__name__.lower(), |
|
745 node.func.id) |
|
746 |
|
747 elif ( |
|
748 nArgs == 1 and |
|
749 isinstance(node.args[0], ast.ListComp) and |
|
750 node.func.id in ('all', 'any', 'enumerate', 'frozenset', |
|
751 'max', 'min', 'sorted', 'sum', 'tuple',) |
|
752 ): |
|
753 self.__error(node.lineno - 1, node.col_offset, "M187", |
|
754 node.func.id) |
|
755 |
|
756 elif ( |
|
757 nArgs == 0 and |
|
758 not any(isinstance(a, ast.Starred) for a in node.args) and |
|
759 not any(k.arg is None for k in node.keywords) and |
|
760 node.func.id in ("tuple", "list", "dict") |
|
761 ): |
|
762 self.__error(node.lineno - 1, node.col_offset, "M186", |
|
763 node.func.id) |
|
764 |
|
765 elif isinstance(node, ast.Compare) and ( |
|
766 len(node.ops) == 1 and |
|
767 isinstance(node.ops[0], ast.In) and |
|
768 len(node.comparators) == 1 and |
|
769 isinstance(node.comparators[0], ast.ListComp) |
|
770 ): |
|
771 self.__error(node.lineno - 1, node.col_offset, "M196") |
|
772 |
|
773 def __checkMutableDefault(self): |
|
774 """ |
|
775 Private method to check for use of mutable types as default arguments. |
|
776 """ |
|
777 mutableTypes = ( |
|
778 ast.Call, |
|
779 ast.Dict, |
|
780 ast.List, |
|
781 ast.Set, |
|
782 ) |
|
783 mutableCalls = ( |
|
784 "Counter", |
|
785 "OrderedDict", |
|
786 "collections.Counter", |
|
787 "collections.OrderedDict", |
|
788 "collections.defaultdict", |
|
789 "collections.deque", |
|
790 "defaultdict", |
|
791 "deque", |
|
792 "dict", |
|
793 "list", |
|
794 "set", |
|
795 ) |
|
796 immutableCalls = ( |
|
797 "tuple", |
|
798 "frozenset", |
|
799 ) |
|
800 functionDefs = [ast.FunctionDef] |
|
801 with contextlib.suppress(AttributeError): |
|
802 functionDefs.append(ast.AsyncFunctionDef) |
|
803 |
|
804 for node in ast.walk(self.__tree): |
|
805 if any(isinstance(node, functionDef) |
|
806 for functionDef in functionDefs): |
|
807 defaults = node.args.defaults[:] |
|
808 with contextlib.suppress(AttributeError): |
|
809 defaults += node.args.kw_defaults[:] |
|
810 for default in defaults: |
|
811 if any(isinstance(default, mutableType) |
|
812 for mutableType in mutableTypes): |
|
813 typeName = type(default).__name__ |
|
814 if isinstance(default, ast.Call): |
|
815 callPath = '.'.join(composeCallPath(default.func)) |
|
816 if callPath in mutableCalls: |
|
817 self.__error(default.lineno - 1, |
|
818 default.col_offset, |
|
819 "M823", callPath + "()") |
|
820 elif callPath not in immutableCalls: |
|
821 self.__error(default.lineno - 1, |
|
822 default.col_offset, |
|
823 "M822", typeName) |
|
824 else: |
|
825 self.__error(default.lineno - 1, |
|
826 default.col_offset, |
|
827 "M821", typeName) |
|
828 |
|
829 def __dictShouldBeChecked(self, node): |
|
830 """ |
|
831 Private function to test, if the node should be checked. |
|
832 |
|
833 @param node reference to the AST node |
|
834 @return flag indicating to check the node |
|
835 @rtype bool |
|
836 """ |
|
837 if not all(AstUtilities.isString(key) for key in node.keys): |
|
838 return False |
|
839 |
|
840 if ( |
|
841 "__IGNORE_WARNING__" in self.__source[node.lineno - 1] or |
|
842 "__IGNORE_WARNING_M201__" in self.__source[node.lineno - 1] |
|
843 ): |
|
844 return False |
|
845 |
|
846 lineNumbers = [key.lineno for key in node.keys] |
|
847 return len(lineNumbers) == len(set(lineNumbers)) |
|
848 |
|
849 def __checkDictWithSortedKeys(self): |
|
850 """ |
|
851 Private method to check, if dictionary keys appear in sorted order. |
|
852 """ |
|
853 for node in ast.walk(self.__tree): |
|
854 if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node): |
|
855 for key1, key2 in zip(node.keys, node.keys[1:]): |
|
856 if key2.s < key1.s: |
|
857 self.__error(key2.lineno - 1, key2.col_offset, |
|
858 "M201", key2.s, key1.s) |
|
859 |
|
860 def __checkLogging(self): |
|
861 """ |
|
862 Private method to check logging statements. |
|
863 """ |
|
864 visitor = LoggingVisitor() |
|
865 visitor.visit(self.__tree) |
|
866 for node, reason in visitor.violations: |
|
867 self.__error(node.lineno - 1, node.col_offset, reason) |
|
868 |
|
869 def __checkGettext(self): |
|
870 """ |
|
871 Private method to check the 'gettext' import statement. |
|
872 """ |
|
873 for node in ast.walk(self.__tree): |
|
874 if ( |
|
875 isinstance(node, ast.ImportFrom) and |
|
876 any(name.asname == '_' for name in node.names) |
|
877 ): |
|
878 self.__error(node.lineno - 1, node.col_offset, "M711", |
|
879 node.names[0].name) |
|
880 |
|
881 def __checkBugBear(self): |
|
882 """ |
|
883 Private method for bugbear checks. |
|
884 """ |
|
885 visitor = BugBearVisitor() |
|
886 visitor.visit(self.__tree) |
|
887 for violation in visitor.violations: |
|
888 node = violation[0] |
|
889 reason = violation[1] |
|
890 params = violation[2:] |
|
891 self.__error(node.lineno - 1, node.col_offset, reason, *params) |
|
892 |
|
893 def __checkReturn(self): |
|
894 """ |
|
895 Private method to check return statements. |
|
896 """ |
|
897 visitor = ReturnVisitor() |
|
898 visitor.visit(self.__tree) |
|
899 for violation in visitor.violations: |
|
900 node = violation[0] |
|
901 reason = violation[1] |
|
902 self.__error(node.lineno - 1, node.col_offset, reason) |
|
903 |
|
904 def __checkDateTime(self): |
|
905 """ |
|
906 Private method to check use of naive datetime functions. |
|
907 """ |
|
908 # step 1: generate an augmented node tree containing parent info |
|
909 # for each child node |
|
910 tree = copy.deepcopy(self.__tree) |
|
911 for node in ast.walk(tree): |
|
912 for childNode in ast.iter_child_nodes(node): |
|
913 childNode._dtCheckerParent = node |
|
914 |
|
915 # step 2: perform checks and report issues |
|
916 visitor = DateTimeVisitor() |
|
917 visitor.visit(tree) |
|
918 for violation in visitor.violations: |
|
919 node = violation[0] |
|
920 reason = violation[1] |
|
921 self.__error(node.lineno - 1, node.col_offset, reason) |
|
922 |
|
923 def __checkSysVersion(self): |
|
924 """ |
|
925 Private method to check the use of sys.version and sys.version_info. |
|
926 """ |
|
927 visitor = SysVersionVisitor() |
|
928 visitor.visit(self.__tree) |
|
929 for violation in visitor.violations: |
|
930 node = violation[0] |
|
931 reason = violation[1] |
|
932 self.__error(node.lineno - 1, node.col_offset, reason) |
|
933 |
|
934 |
|
935 class TextVisitor(ast.NodeVisitor): |
|
936 """ |
|
937 Class implementing a node visitor for bytes and str instances. |
|
938 |
|
939 It tries to detect docstrings as string of the first expression of each |
|
940 module, class or function. |
|
941 """ |
|
942 # modelled after the string format flake8 extension |
|
943 |
|
944 def __init__(self): |
|
945 """ |
|
946 Constructor |
|
947 """ |
|
948 super().__init__() |
|
949 self.nodes = [] |
|
950 self.calls = {} |
|
951 |
|
952 def __addNode(self, node): |
|
953 """ |
|
954 Private method to add a node to our list of nodes. |
|
955 |
|
956 @param node reference to the node to add |
|
957 @type ast.AST |
|
958 """ |
|
959 if not hasattr(node, 'is_docstring'): |
|
960 node.is_docstring = False |
|
961 self.nodes.append(node) |
|
962 |
|
963 def visit_Str(self, node): |
|
964 """ |
|
965 Public method to record a string node. |
|
966 |
|
967 @param node reference to the string node |
|
968 @type ast.Str |
|
969 """ |
|
970 self.__addNode(node) |
|
971 |
|
972 def visit_Bytes(self, node): |
|
973 """ |
|
974 Public method to record a bytes node. |
|
975 |
|
976 @param node reference to the bytes node |
|
977 @type ast.Bytes |
|
978 """ |
|
979 self.__addNode(node) |
|
980 |
|
981 def visit_Constant(self, node): |
|
982 """ |
|
983 Public method to handle constant nodes. |
|
984 |
|
985 @param node reference to the bytes node |
|
986 @type ast.Constant |
|
987 """ |
|
988 if sys.version_info >= (3, 8, 0): |
|
989 if AstUtilities.isBaseString(node): |
|
990 self.__addNode(node) |
|
991 else: |
|
992 super().generic_visit(node) |
|
993 else: |
|
994 super().generic_visit(node) |
|
995 |
|
996 def __visitDefinition(self, node): |
|
997 """ |
|
998 Private method handling class and function definitions. |
|
999 |
|
1000 @param node reference to the node to handle |
|
1001 @type ast.FunctionDef, ast.AsyncFunctionDef or ast.ClassDef |
|
1002 """ |
|
1003 # Manually traverse class or function definition |
|
1004 # * Handle decorators normally |
|
1005 # * Use special check for body content |
|
1006 # * Don't handle the rest (e.g. bases) |
|
1007 for decorator in node.decorator_list: |
|
1008 self.visit(decorator) |
|
1009 self.__visitBody(node) |
|
1010 |
|
1011 def __visitBody(self, node): |
|
1012 """ |
|
1013 Private method to traverse the body of the node manually. |
|
1014 |
|
1015 If the first node is an expression which contains a string or bytes it |
|
1016 marks that as a docstring. |
|
1017 |
|
1018 @param node reference to the node to traverse |
|
1019 @type ast.AST |
|
1020 """ |
|
1021 if ( |
|
1022 node.body and |
|
1023 isinstance(node.body[0], ast.Expr) and |
|
1024 AstUtilities.isBaseString(node.body[0].value) |
|
1025 ): |
|
1026 node.body[0].value.is_docstring = True |
|
1027 |
|
1028 for subnode in node.body: |
|
1029 self.visit(subnode) |
|
1030 |
|
1031 def visit_Module(self, node): |
|
1032 """ |
|
1033 Public method to handle a module. |
|
1034 |
|
1035 @param node reference to the node to handle |
|
1036 @type ast.Module |
|
1037 """ |
|
1038 self.__visitBody(node) |
|
1039 |
|
1040 def visit_ClassDef(self, node): |
|
1041 """ |
|
1042 Public method to handle a class definition. |
|
1043 |
|
1044 @param node reference to the node to handle |
|
1045 @type ast.ClassDef |
|
1046 """ |
|
1047 # Skipped nodes: ('name', 'bases', 'keywords', 'starargs', 'kwargs') |
|
1048 self.__visitDefinition(node) |
|
1049 |
|
1050 def visit_FunctionDef(self, node): |
|
1051 """ |
|
1052 Public method to handle a function definition. |
|
1053 |
|
1054 @param node reference to the node to handle |
|
1055 @type ast.FunctionDef |
|
1056 """ |
|
1057 # Skipped nodes: ('name', 'args', 'returns') |
|
1058 self.__visitDefinition(node) |
|
1059 |
|
1060 def visit_AsyncFunctionDef(self, node): |
|
1061 """ |
|
1062 Public method to handle an asynchronous function definition. |
|
1063 |
|
1064 @param node reference to the node to handle |
|
1065 @type ast.AsyncFunctionDef |
|
1066 """ |
|
1067 # Skipped nodes: ('name', 'args', 'returns') |
|
1068 self.__visitDefinition(node) |
|
1069 |
|
1070 def visit_Call(self, node): |
|
1071 """ |
|
1072 Public method to handle a function call. |
|
1073 |
|
1074 @param node reference to the node to handle |
|
1075 @type ast.Call |
|
1076 """ |
|
1077 if ( |
|
1078 isinstance(node.func, ast.Attribute) and |
|
1079 node.func.attr == 'format' |
|
1080 ): |
|
1081 if AstUtilities.isBaseString(node.func.value): |
|
1082 self.calls[node.func.value] = (node, False) |
|
1083 elif ( |
|
1084 isinstance(node.func.value, ast.Name) and |
|
1085 node.func.value.id == 'str' and |
|
1086 node.args and |
|
1087 AstUtilities.isBaseString(node.args[0]) |
|
1088 ): |
|
1089 self.calls[node.args[0]] = (node, True) |
|
1090 super().generic_visit(node) |
|
1091 |
|
1092 |
|
1093 class LoggingVisitor(ast.NodeVisitor): |
|
1094 """ |
|
1095 Class implementing a node visitor to check logging statements. |
|
1096 """ |
|
1097 LoggingLevels = { |
|
1098 "debug", |
|
1099 "critical", |
|
1100 "error", |
|
1101 "info", |
|
1102 "warn", |
|
1103 "warning", |
|
1104 } |
|
1105 |
|
1106 def __init__(self): |
|
1107 """ |
|
1108 Constructor |
|
1109 """ |
|
1110 super().__init__() |
|
1111 |
|
1112 self.__currentLoggingCall = None |
|
1113 self.__currentLoggingArgument = None |
|
1114 self.__currentLoggingLevel = None |
|
1115 self.__currentExtraKeyword = None |
|
1116 self.violations = [] |
|
1117 |
|
1118 def __withinLoggingStatement(self): |
|
1119 """ |
|
1120 Private method to check, if we are inside a logging statement. |
|
1121 |
|
1122 @return flag indicating we are inside a logging statement |
|
1123 @rtype bool |
|
1124 """ |
|
1125 return self.__currentLoggingCall is not None |
|
1126 |
|
1127 def __withinLoggingArgument(self): |
|
1128 """ |
|
1129 Private method to check, if we are inside a logging argument. |
|
1130 |
|
1131 @return flag indicating we are inside a logging argument |
|
1132 @rtype bool |
|
1133 """ |
|
1134 return self.__currentLoggingArgument is not None |
|
1135 |
|
1136 def __withinExtraKeyword(self, node): |
|
1137 """ |
|
1138 Private method to check, if we are inside the extra keyword. |
|
1139 |
|
1140 @param node reference to the node to be checked |
|
1141 @type ast.keyword |
|
1142 @return flag indicating we are inside the extra keyword |
|
1143 @rtype bool |
|
1144 """ |
|
1145 return ( |
|
1146 self.__currentExtraKeyword is not None and |
|
1147 self.__currentExtraKeyword != node |
|
1148 ) |
|
1149 |
|
1150 def __detectLoggingLevel(self, node): |
|
1151 """ |
|
1152 Private method to decide whether an AST Call is a logging call. |
|
1153 |
|
1154 @param node reference to the node to be processed |
|
1155 @type ast.Call |
|
1156 @return logging level |
|
1157 @rtype str or None |
|
1158 """ |
|
1159 with contextlib.suppress(AttributeError): |
|
1160 if node.func.value.id == "warnings": |
|
1161 return None |
|
1162 |
|
1163 if node.func.attr in LoggingVisitor.LoggingLevels: |
|
1164 return node.func.attr |
|
1165 |
|
1166 return None |
|
1167 |
|
1168 def __isFormatCall(self, node): |
|
1169 """ |
|
1170 Private method to check if a function call uses format. |
|
1171 |
|
1172 @param node reference to the node to be processed |
|
1173 @type ast.Call |
|
1174 @return flag indicating the function call uses format |
|
1175 @rtype bool |
|
1176 """ |
|
1177 try: |
|
1178 return node.func.attr == "format" |
|
1179 except AttributeError: |
|
1180 return False |
|
1181 |
|
1182 def visit_Call(self, node): |
|
1183 """ |
|
1184 Public method to handle a function call. |
|
1185 |
|
1186 Every logging statement and string format is expected to be a function |
|
1187 call. |
|
1188 |
|
1189 @param node reference to the node to be processed |
|
1190 @type ast.Call |
|
1191 """ |
|
1192 # we are in a logging statement |
|
1193 if ( |
|
1194 self.__withinLoggingStatement() and |
|
1195 self.__withinLoggingArgument() and |
|
1196 self.__isFormatCall(node) |
|
1197 ): |
|
1198 self.violations.append((node, "M651")) |
|
1199 super().generic_visit(node) |
|
1200 return |
|
1201 |
|
1202 loggingLevel = self.__detectLoggingLevel(node) |
|
1203 |
|
1204 if loggingLevel and self.__currentLoggingLevel is None: |
|
1205 self.__currentLoggingLevel = loggingLevel |
|
1206 |
|
1207 # we are in some other statement |
|
1208 if loggingLevel is None: |
|
1209 super().generic_visit(node) |
|
1210 return |
|
1211 |
|
1212 # we are entering a new logging statement |
|
1213 self.__currentLoggingCall = node |
|
1214 |
|
1215 if loggingLevel == "warn": |
|
1216 self.violations.append((node, "M655")) |
|
1217 |
|
1218 for index, child in enumerate(ast.iter_child_nodes(node)): |
|
1219 if index == 1: |
|
1220 self.__currentLoggingArgument = child |
|
1221 if ( |
|
1222 index > 1 and |
|
1223 isinstance(child, ast.keyword) and |
|
1224 child.arg == "extra" |
|
1225 ): |
|
1226 self.__currentExtraKeyword = child |
|
1227 |
|
1228 super().visit(child) |
|
1229 |
|
1230 self.__currentLoggingArgument = None |
|
1231 self.__currentExtraKeyword = None |
|
1232 |
|
1233 self.__currentLoggingCall = None |
|
1234 self.__currentLoggingLevel = None |
|
1235 |
|
1236 def visit_BinOp(self, node): |
|
1237 """ |
|
1238 Public method to handle binary operations while processing the first |
|
1239 logging argument. |
|
1240 |
|
1241 @param node reference to the node to be processed |
|
1242 @type ast.BinOp |
|
1243 """ |
|
1244 if self.__withinLoggingStatement() and self.__withinLoggingArgument(): |
|
1245 # handle percent format |
|
1246 if isinstance(node.op, ast.Mod): |
|
1247 self.violations.append((node, "M652")) |
|
1248 |
|
1249 # handle string concat |
|
1250 if isinstance(node.op, ast.Add): |
|
1251 self.violations.append((node, "M653")) |
|
1252 |
|
1253 super().generic_visit(node) |
|
1254 |
|
1255 def visit_JoinedStr(self, node): |
|
1256 """ |
|
1257 Public method to handle f-string arguments. |
|
1258 |
|
1259 @param node reference to the node to be processed |
|
1260 @type ast.JoinedStr |
|
1261 """ |
|
1262 if ( |
|
1263 self.__withinLoggingStatement() and |
|
1264 any(isinstance(i, ast.FormattedValue) for i in node.values) and |
|
1265 self.__withinLoggingArgument() |
|
1266 ): |
|
1267 self.violations.append((node, "M654")) |
|
1268 |
|
1269 super().generic_visit(node) |
|
1270 |
|
1271 |
|
1272 class BugBearVisitor(ast.NodeVisitor): |
|
1273 """ |
|
1274 Class implementing a node visitor to check for various topics. |
|
1275 """ |
|
1276 # |
|
1277 # This class was implemented along the BugBear flake8 extension (v 19.3.0). |
|
1278 # Original: Copyright (c) 2016 Łukasz Langa |
|
1279 # |
|
1280 |
|
1281 NodeWindowSize = 4 |
|
1282 |
|
1283 def __init__(self): |
|
1284 """ |
|
1285 Constructor |
|
1286 """ |
|
1287 super().__init__() |
|
1288 |
|
1289 self.__nodeStack = [] |
|
1290 self.__nodeWindow = [] |
|
1291 self.violations = [] |
|
1292 |
|
1293 def visit(self, node): |
|
1294 """ |
|
1295 Public method to traverse a given AST node. |
|
1296 |
|
1297 @param node AST node to be traversed |
|
1298 @type ast.Node |
|
1299 """ |
|
1300 self.__nodeStack.append(node) |
|
1301 self.__nodeWindow.append(node) |
|
1302 self.__nodeWindow = self.__nodeWindow[-BugBearVisitor.NodeWindowSize:] |
|
1303 |
|
1304 super().visit(node) |
|
1305 |
|
1306 self.__nodeStack.pop() |
|
1307 |
|
1308 def visit_UAdd(self, node): |
|
1309 """ |
|
1310 Public method to handle unary additions. |
|
1311 |
|
1312 @param node reference to the node to be processed |
|
1313 @type ast.UAdd |
|
1314 """ |
|
1315 trailingNodes = list(map(type, self.__nodeWindow[-4:])) |
|
1316 if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]: |
|
1317 originator = self.__nodeWindow[-4] |
|
1318 self.violations.append((originator, "M501")) |
|
1319 |
|
1320 self.generic_visit(node) |
|
1321 |
|
1322 def visit_Call(self, node): |
|
1323 """ |
|
1324 Public method to handle a function call. |
|
1325 |
|
1326 @param node reference to the node to be processed |
|
1327 @type ast.Call |
|
1328 """ |
|
1329 validPaths = ("six", "future.utils", "builtins") |
|
1330 methodsDict = { |
|
1331 "M521": ("iterkeys", "itervalues", "iteritems", "iterlists"), |
|
1332 "M522": ("viewkeys", "viewvalues", "viewitems", "viewlists"), |
|
1333 "M523": ("next",), |
|
1334 } |
|
1335 |
|
1336 if isinstance(node.func, ast.Attribute): |
|
1337 for code, methods in methodsDict.items(): |
|
1338 if node.func.attr in methods: |
|
1339 callPath = ".".join(composeCallPath(node.func.value)) |
|
1340 if callPath not in validPaths: |
|
1341 self.violations.append((node, code)) |
|
1342 break |
|
1343 else: |
|
1344 self.__checkForM502(node) |
|
1345 else: |
|
1346 with contextlib.suppress(AttributeError, IndexError): |
|
1347 # bad super() call |
|
1348 if isinstance(node.func, ast.Name) and node.func.id == "super": |
|
1349 args = node.args |
|
1350 if ( |
|
1351 len(args) == 2 and |
|
1352 isinstance(args[0], ast.Attribute) and |
|
1353 isinstance(args[0].value, ast.Name) and |
|
1354 args[0].value.id == 'self' and |
|
1355 args[0].attr == '__class__' |
|
1356 ): |
|
1357 self.violations.append((node, "M509")) |
|
1358 |
|
1359 # bad getattr and setattr |
|
1360 if ( |
|
1361 node.func.id in ("getattr", "hasattr") and |
|
1362 node.args[1].s == "__call__" |
|
1363 ): |
|
1364 self.violations.append((node, "M511")) |
|
1365 if ( |
|
1366 node.func.id == "getattr" and |
|
1367 len(node.args) == 2 and |
|
1368 AstUtilities.isString(node.args[1]) |
|
1369 ): |
|
1370 self.violations.append((node, "M512")) |
|
1371 elif ( |
|
1372 node.func.id == "setattr" and |
|
1373 len(node.args) == 3 and |
|
1374 AstUtilities.isString(node.args[1]) |
|
1375 ): |
|
1376 self.violations.append((node, "M513")) |
|
1377 |
|
1378 self.generic_visit(node) |
|
1379 |
|
1380 def visit_Attribute(self, node): |
|
1381 """ |
|
1382 Public method to handle attributes. |
|
1383 |
|
1384 @param node reference to the node to be processed |
|
1385 @type ast.Attribute |
|
1386 """ |
|
1387 callPath = list(composeCallPath(node)) |
|
1388 |
|
1389 if '.'.join(callPath) == 'sys.maxint': |
|
1390 self.violations.append((node, "M504")) |
|
1391 |
|
1392 elif ( |
|
1393 len(callPath) == 2 and |
|
1394 callPath[1] == 'message' |
|
1395 ): |
|
1396 name = callPath[0] |
|
1397 for elem in reversed(self.__nodeStack[:-1]): |
|
1398 if isinstance(elem, ast.ExceptHandler) and elem.name == name: |
|
1399 self.violations.append((node, "M505")) |
|
1400 break |
|
1401 |
|
1402 def visit_Assign(self, node): |
|
1403 """ |
|
1404 Public method to handle assignments. |
|
1405 |
|
1406 @param node reference to the node to be processed |
|
1407 @type ast.Assign |
|
1408 """ |
|
1409 if isinstance(self.__nodeStack[-2], ast.ClassDef): |
|
1410 # By using 'hasattr' below we're ignoring starred arguments, slices |
|
1411 # and tuples for simplicity. |
|
1412 assignTargets = {t.id for t in node.targets if hasattr(t, 'id')} |
|
1413 if '__metaclass__' in assignTargets: |
|
1414 self.violations.append((node, "M524")) |
|
1415 |
|
1416 elif len(node.targets) == 1: |
|
1417 target = node.targets[0] |
|
1418 if ( |
|
1419 isinstance(target, ast.Attribute) and |
|
1420 isinstance(target.value, ast.Name) and |
|
1421 (target.value.id, target.attr) == ('os', 'environ') |
|
1422 ): |
|
1423 self.violations.append((node, "M506")) |
|
1424 |
|
1425 self.generic_visit(node) |
|
1426 |
|
1427 def visit_For(self, node): |
|
1428 """ |
|
1429 Public method to handle 'for' statements. |
|
1430 |
|
1431 @param node reference to the node to be processed |
|
1432 @type ast.For |
|
1433 """ |
|
1434 self.__checkForM507(node) |
|
1435 |
|
1436 self.generic_visit(node) |
|
1437 |
|
1438 def visit_AsyncFor(self, node): |
|
1439 """ |
|
1440 Public method to handle 'for' statements. |
|
1441 |
|
1442 @param node reference to the node to be processed |
|
1443 @type ast.AsyncFor |
|
1444 """ |
|
1445 self.__checkForM507(node) |
|
1446 |
|
1447 self.generic_visit(node) |
|
1448 |
|
1449 def visit_Assert(self, node): |
|
1450 """ |
|
1451 Public method to handle 'assert' statements. |
|
1452 |
|
1453 @param node reference to the node to be processed |
|
1454 @type ast.Assert |
|
1455 """ |
|
1456 if ( |
|
1457 AstUtilities.isNameConstant(node.test) and |
|
1458 AstUtilities.getValue(node.test) is False |
|
1459 ): |
|
1460 self.violations.append((node, "M503")) |
|
1461 |
|
1462 self.generic_visit(node) |
|
1463 |
|
1464 def visit_JoinedStr(self, node): |
|
1465 """ |
|
1466 Public method to handle f-string arguments. |
|
1467 |
|
1468 @param node reference to the node to be processed |
|
1469 @type ast.JoinedStr |
|
1470 """ |
|
1471 for value in node.values: |
|
1472 if isinstance(value, ast.FormattedValue): |
|
1473 return |
|
1474 |
|
1475 self.violations.append((node, "M508")) |
|
1476 |
|
1477 def __checkForM502(self, node): |
|
1478 """ |
|
1479 Private method to check the use of *strip(). |
|
1480 |
|
1481 @param node reference to the node to be processed |
|
1482 @type ast.Call |
|
1483 """ |
|
1484 if node.func.attr not in ("lstrip", "rstrip", "strip"): |
|
1485 return # method name doesn't match |
|
1486 |
|
1487 if len(node.args) != 1 or not AstUtilities.isString(node.args[0]): |
|
1488 return # used arguments don't match the builtin strip |
|
1489 |
|
1490 s = AstUtilities.getValue(node.args[0]) |
|
1491 if len(s) == 1: |
|
1492 return # stripping just one character |
|
1493 |
|
1494 if len(s) == len(set(s)): |
|
1495 return # no characters appear more than once |
|
1496 |
|
1497 self.violations.append((node, "M502")) |
|
1498 |
|
1499 def __checkForM507(self, node): |
|
1500 """ |
|
1501 Private method to check for unused loop variables. |
|
1502 |
|
1503 @param node reference to the node to be processed |
|
1504 @type ast.For |
|
1505 """ |
|
1506 targets = NameFinder() |
|
1507 targets.visit(node.target) |
|
1508 ctrlNames = set(filter(lambda s: not s.startswith('_'), |
|
1509 targets.getNames())) |
|
1510 body = NameFinder() |
|
1511 for expr in node.body: |
|
1512 body.visit(expr) |
|
1513 usedNames = set(body.getNames()) |
|
1514 for name in sorted(ctrlNames - usedNames): |
|
1515 n = targets.getNames()[name][0] |
|
1516 self.violations.append((n, "M507", name)) |
|
1517 |
|
1518 |
|
1519 class NameFinder(ast.NodeVisitor): |
|
1520 """ |
|
1521 Class to extract a name out of a tree of nodes. |
|
1522 """ |
|
1523 def __init__(self): |
|
1524 """ |
|
1525 Constructor |
|
1526 """ |
|
1527 super().__init__() |
|
1528 |
|
1529 self.__names = {} |
|
1530 |
|
1531 def visit_Name(self, node): |
|
1532 """ |
|
1533 Public method to handle 'Name' nodes. |
|
1534 |
|
1535 @param node reference to the node to be processed |
|
1536 @type ast.Name |
|
1537 """ |
|
1538 self.__names.setdefault(node.id, []).append(node) |
|
1539 |
|
1540 def visit(self, node): |
|
1541 """ |
|
1542 Public method to traverse a given AST node. |
|
1543 |
|
1544 @param node AST node to be traversed |
|
1545 @type ast.Node |
|
1546 """ |
|
1547 if isinstance(node, list): |
|
1548 for elem in node: |
|
1549 super().visit(elem) |
|
1550 else: |
|
1551 super().visit(node) |
|
1552 |
|
1553 def getNames(self): |
|
1554 """ |
|
1555 Public method to return the extracted names and Name nodes. |
|
1556 |
|
1557 @return dictionary containing the names as keys and the list of nodes |
|
1558 @rtype dict |
|
1559 """ |
|
1560 return self.__names |
|
1561 |
|
1562 |
|
1563 class ReturnVisitor(ast.NodeVisitor): |
|
1564 """ |
|
1565 Class implementing a node visitor to check return statements. |
|
1566 """ |
|
1567 Assigns = 'assigns' |
|
1568 Refs = 'refs' |
|
1569 Returns = 'returns' |
|
1570 |
|
1571 def __init__(self): |
|
1572 """ |
|
1573 Constructor |
|
1574 """ |
|
1575 super().__init__() |
|
1576 |
|
1577 self.__stack = [] |
|
1578 self.violations = [] |
|
1579 self.__loopCount = 0 |
|
1580 |
|
1581 @property |
|
1582 def assigns(self): |
|
1583 """ |
|
1584 Public method to get the Assign nodes. |
|
1585 |
|
1586 @return dictionary containing the node name as key and line number |
|
1587 as value |
|
1588 @rtype dict |
|
1589 """ |
|
1590 return self.__stack[-1][ReturnVisitor.Assigns] |
|
1591 |
|
1592 @property |
|
1593 def refs(self): |
|
1594 """ |
|
1595 Public method to get the References nodes. |
|
1596 |
|
1597 @return dictionary containing the node name as key and line number |
|
1598 as value |
|
1599 @rtype dict |
|
1600 """ |
|
1601 return self.__stack[-1][ReturnVisitor.Refs] |
|
1602 |
|
1603 @property |
|
1604 def returns(self): |
|
1605 """ |
|
1606 Public method to get the Return nodes. |
|
1607 |
|
1608 @return dictionary containing the node name as key and line number |
|
1609 as value |
|
1610 @rtype dict |
|
1611 """ |
|
1612 return self.__stack[-1][ReturnVisitor.Returns] |
|
1613 |
|
1614 def visit_For(self, node): |
|
1615 """ |
|
1616 Public method to handle a for loop. |
|
1617 |
|
1618 @param node reference to the for node to handle |
|
1619 @type ast.For |
|
1620 """ |
|
1621 self.__visitLoop(node) |
|
1622 |
|
1623 def visit_AsyncFor(self, node): |
|
1624 """ |
|
1625 Public method to handle an async for loop. |
|
1626 |
|
1627 @param node reference to the async for node to handle |
|
1628 @type ast.AsyncFor |
|
1629 """ |
|
1630 self.__visitLoop(node) |
|
1631 |
|
1632 def visit_While(self, node): |
|
1633 """ |
|
1634 Public method to handle a while loop. |
|
1635 |
|
1636 @param node reference to the while node to handle |
|
1637 @type ast.While |
|
1638 """ |
|
1639 self.__visitLoop(node) |
|
1640 |
|
1641 def __visitLoop(self, node): |
|
1642 """ |
|
1643 Private method to handle loop nodes. |
|
1644 |
|
1645 @param node reference to the loop node to handle |
|
1646 @type ast.For, ast.AsyncFor or ast.While |
|
1647 """ |
|
1648 self.__loopCount += 1 |
|
1649 self.generic_visit(node) |
|
1650 self.__loopCount -= 1 |
|
1651 |
|
1652 def __visitWithStack(self, node): |
|
1653 """ |
|
1654 Private method to traverse a given function node using a stack. |
|
1655 |
|
1656 @param node AST node to be traversed |
|
1657 @type ast.FunctionDef or ast.AsyncFunctionDef |
|
1658 """ |
|
1659 self.__stack.append({ |
|
1660 ReturnVisitor.Assigns: defaultdict(list), |
|
1661 ReturnVisitor.Refs: defaultdict(list), |
|
1662 ReturnVisitor.Returns: [] |
|
1663 }) |
|
1664 |
|
1665 self.generic_visit(node) |
|
1666 self.__checkFunction(node) |
|
1667 self.__stack.pop() |
|
1668 |
|
1669 def visit_FunctionDef(self, node): |
|
1670 """ |
|
1671 Public method to handle a function definition. |
|
1672 |
|
1673 @param node reference to the node to handle |
|
1674 @type ast.FunctionDef |
|
1675 """ |
|
1676 self.__visitWithStack(node) |
|
1677 |
|
1678 def visit_AsyncFunctionDef(self, node): |
|
1679 """ |
|
1680 Public method to handle a function definition. |
|
1681 |
|
1682 @param node reference to the node to handle |
|
1683 @type ast.AsyncFunctionDef |
|
1684 """ |
|
1685 self.__visitWithStack(node) |
|
1686 |
|
1687 def visit_Return(self, node): |
|
1688 """ |
|
1689 Public method to handle a return node. |
|
1690 |
|
1691 @param node reference to the node to handle |
|
1692 @type ast.Return |
|
1693 """ |
|
1694 self.returns.append(node) |
|
1695 self.generic_visit(node) |
|
1696 |
|
1697 def visit_Assign(self, node): |
|
1698 """ |
|
1699 Public method to handle an assign node. |
|
1700 |
|
1701 @param node reference to the node to handle |
|
1702 @type ast.Assign |
|
1703 """ |
|
1704 if not self.__stack: |
|
1705 return |
|
1706 |
|
1707 self.generic_visit(node.value) |
|
1708 |
|
1709 target = node.targets[0] |
|
1710 if ( |
|
1711 isinstance(target, ast.Tuple) and |
|
1712 not isinstance(node.value, ast.Tuple) |
|
1713 ): |
|
1714 # skip unpacking assign |
|
1715 return |
|
1716 |
|
1717 self.__visitAssignTarget(target) |
|
1718 |
|
1719 def visit_Name(self, node): |
|
1720 """ |
|
1721 Public method to handle a name node. |
|
1722 |
|
1723 @param node reference to the node to handle |
|
1724 @type ast.Name |
|
1725 """ |
|
1726 if self.__stack: |
|
1727 self.refs[node.id].append(node.lineno) |
|
1728 |
|
1729 def __visitAssignTarget(self, node): |
|
1730 """ |
|
1731 Private method to handle an assign target node. |
|
1732 |
|
1733 @param node reference to the node to handle |
|
1734 @type ast.AST |
|
1735 """ |
|
1736 if isinstance(node, ast.Tuple): |
|
1737 for elt in node.elts: |
|
1738 self.__visitAssignTarget(elt) |
|
1739 return |
|
1740 |
|
1741 if not self.__loopCount and isinstance(node, ast.Name): |
|
1742 self.assigns[node.id].append(node.lineno) |
|
1743 return |
|
1744 |
|
1745 self.generic_visit(node) |
|
1746 |
|
1747 def __checkFunction(self, node): |
|
1748 """ |
|
1749 Private method to check a function definition node. |
|
1750 |
|
1751 @param node reference to the node to check |
|
1752 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
1753 """ |
|
1754 if not self.returns or not node.body: |
|
1755 return |
|
1756 |
|
1757 if len(node.body) == 1 and isinstance(node.body[-1], ast.Return): |
|
1758 # skip functions that consist of `return None` only |
|
1759 return |
|
1760 |
|
1761 if not self.__resultExists(): |
|
1762 self.__checkUnnecessaryReturnNone() |
|
1763 return |
|
1764 |
|
1765 self.__checkImplicitReturnValue() |
|
1766 self.__checkImplicitReturn(node.body[-1]) |
|
1767 |
|
1768 for n in self.returns: |
|
1769 if n.value: |
|
1770 self.__checkUnnecessaryAssign(n.value) |
|
1771 |
|
1772 def __isNone(self, node): |
|
1773 """ |
|
1774 Private method to check, if a node value is None. |
|
1775 |
|
1776 @param node reference to the node to check |
|
1777 @type ast.AST |
|
1778 @return flag indicating the node contains a None value |
|
1779 @rtype bool |
|
1780 """ |
|
1781 return ( |
|
1782 AstUtilities.isNameConstant(node) and |
|
1783 AstUtilities.getValue(node) is None |
|
1784 ) |
|
1785 |
|
1786 def __isFalse(self, node): |
|
1787 """ |
|
1788 Private method to check, if a node value is False. |
|
1789 |
|
1790 @param node reference to the node to check |
|
1791 @type ast.AST |
|
1792 @return flag indicating the node contains a False value |
|
1793 @rtype bool |
|
1794 """ |
|
1795 return ( |
|
1796 AstUtilities.isNameConstant(node) and |
|
1797 AstUtilities.getValue(node) is False |
|
1798 ) |
|
1799 |
|
1800 def __resultExists(self): |
|
1801 """ |
|
1802 Private method to check the existance of a return result. |
|
1803 |
|
1804 @return flag indicating the existence of a return result |
|
1805 @rtype bool |
|
1806 """ |
|
1807 for node in self.returns: |
|
1808 value = node.value |
|
1809 if value and not self.__isNone(value): |
|
1810 return True |
|
1811 |
|
1812 return False |
|
1813 |
|
1814 def __checkImplicitReturnValue(self): |
|
1815 """ |
|
1816 Private method to check for implicit return values. |
|
1817 """ |
|
1818 for node in self.returns: |
|
1819 if not node.value: |
|
1820 self.violations.append((node, "M832")) |
|
1821 |
|
1822 def __checkUnnecessaryReturnNone(self): |
|
1823 """ |
|
1824 Private method to check for an unnecessary 'return None' statement. |
|
1825 """ |
|
1826 for node in self.returns: |
|
1827 if self.__isNone(node.value): |
|
1828 self.violations.append((node, "M831")) |
|
1829 |
|
1830 def __checkImplicitReturn(self, node): |
|
1831 """ |
|
1832 Private method to check for an implicit return statement. |
|
1833 |
|
1834 @param node reference to the node to check |
|
1835 @type ast.AST |
|
1836 """ |
|
1837 if isinstance(node, ast.If): |
|
1838 if not node.body or not node.orelse: |
|
1839 self.violations.append((node, "M833")) |
|
1840 return |
|
1841 |
|
1842 self.__checkImplicitReturn(node.body[-1]) |
|
1843 self.__checkImplicitReturn(node.orelse[-1]) |
|
1844 return |
|
1845 |
|
1846 if isinstance(node, (ast.For, ast.AsyncFor)) and node.orelse: |
|
1847 self.__checkImplicitReturn(node.orelse[-1]) |
|
1848 return |
|
1849 |
|
1850 if isinstance(node, (ast.With, ast.AsyncWith)): |
|
1851 self.__checkImplicitReturn(node.body[-1]) |
|
1852 return |
|
1853 |
|
1854 if isinstance(node, ast.Assert) and self.__isFalse(node.test): |
|
1855 return |
|
1856 |
|
1857 try: |
|
1858 okNodes = (ast.Return, ast.Raise, ast.While, ast.Try) |
|
1859 except AttributeError: |
|
1860 okNodes = (ast.Return, ast.Raise, ast.While) |
|
1861 if not isinstance(node, okNodes): |
|
1862 self.violations.append((node, "M833")) |
|
1863 |
|
1864 def __checkUnnecessaryAssign(self, node): |
|
1865 """ |
|
1866 Private method to check for an unnecessary assign statement. |
|
1867 |
|
1868 @param node reference to the node to check |
|
1869 @type ast.AST |
|
1870 """ |
|
1871 if not isinstance(node, ast.Name): |
|
1872 return |
|
1873 |
|
1874 varname = node.id |
|
1875 returnLineno = node.lineno |
|
1876 |
|
1877 if varname not in self.assigns: |
|
1878 return |
|
1879 |
|
1880 if varname not in self.refs: |
|
1881 self.violations.append((node, "M834")) |
|
1882 return |
|
1883 |
|
1884 if self.__hasRefsBeforeNextAssign(varname, returnLineno): |
|
1885 return |
|
1886 |
|
1887 self.violations.append((node, "M834")) |
|
1888 |
|
1889 def __hasRefsBeforeNextAssign(self, varname, returnLineno): |
|
1890 """ |
|
1891 Private method to check for references before a following assign |
|
1892 statement. |
|
1893 |
|
1894 @param varname variable name to check for |
|
1895 @type str |
|
1896 @param returnLineno line number of the return statement |
|
1897 @type int |
|
1898 @return flag indicating the existence of references |
|
1899 @rtype bool |
|
1900 """ |
|
1901 beforeAssign = 0 |
|
1902 afterAssign = None |
|
1903 |
|
1904 for lineno in sorted(self.assigns[varname]): |
|
1905 if lineno > returnLineno: |
|
1906 afterAssign = lineno |
|
1907 break |
|
1908 |
|
1909 if lineno <= returnLineno: |
|
1910 beforeAssign = lineno |
|
1911 |
|
1912 for lineno in self.refs[varname]: |
|
1913 if lineno == returnLineno: |
|
1914 continue |
|
1915 |
|
1916 if afterAssign: |
|
1917 if beforeAssign < lineno <= afterAssign: |
|
1918 return True |
|
1919 |
|
1920 elif beforeAssign < lineno: |
|
1921 return True |
|
1922 |
|
1923 return False |
|
1924 |
|
1925 |
|
1926 class DateTimeVisitor(ast.NodeVisitor): |
|
1927 """ |
|
1928 Class implementing a node visitor to check datetime function calls. |
|
1929 |
|
1930 Note: This class is modelled after flake8_datetimez checker. |
|
1931 """ |
|
1932 def __init__(self): |
|
1933 """ |
|
1934 Constructor |
|
1935 """ |
|
1936 super().__init__() |
|
1937 |
|
1938 self.violations = [] |
|
1939 |
|
1940 def __getFromKeywords(self, keywords, name): |
|
1941 """ |
|
1942 Private method to get a keyword node given its name. |
|
1943 |
|
1944 @param keywords list of keyword argument nodes |
|
1945 @type list of ast.AST |
|
1946 @param name name of the keyword node |
|
1947 @type str |
|
1948 @return keyword node |
|
1949 @rtype ast.AST |
|
1950 """ |
|
1951 for keyword in keywords: |
|
1952 if keyword.arg == name: |
|
1953 return keyword |
|
1954 |
|
1955 return None |
|
1956 |
|
1957 def visit_Call(self, node): |
|
1958 """ |
|
1959 Public method to handle a function call. |
|
1960 |
|
1961 Every datetime related function call is check for use of the naive |
|
1962 variant (i.e. use without TZ info). |
|
1963 |
|
1964 @param node reference to the node to be processed |
|
1965 @type ast.Call |
|
1966 """ |
|
1967 # datetime.something() |
|
1968 isDateTimeClass = ( |
|
1969 isinstance(node.func, ast.Attribute) and |
|
1970 isinstance(node.func.value, ast.Name) and |
|
1971 node.func.value.id == 'datetime') |
|
1972 |
|
1973 # datetime.datetime.something() |
|
1974 isDateTimeModuleAndClass = ( |
|
1975 isinstance(node.func, ast.Attribute) and |
|
1976 isinstance(node.func.value, ast.Attribute) and |
|
1977 node.func.value.attr == 'datetime' and |
|
1978 isinstance(node.func.value.value, ast.Name) and |
|
1979 node.func.value.value.id == 'datetime') |
|
1980 |
|
1981 if isDateTimeClass: |
|
1982 if node.func.attr == 'datetime': |
|
1983 # datetime.datetime(2000, 1, 1, 0, 0, 0, 0, |
|
1984 # datetime.timezone.utc) |
|
1985 isCase1 = ( |
|
1986 len(node.args) >= 8 and |
|
1987 not ( |
|
1988 AstUtilities.isNameConstant(node.args[7]) and |
|
1989 AstUtilities.getValue(node.args[7]) is None |
|
1990 ) |
|
1991 ) |
|
1992 |
|
1993 # datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc) |
|
1994 tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo') |
|
1995 isCase2 = ( |
|
1996 tzinfoKeyword is not None and |
|
1997 not ( |
|
1998 AstUtilities.isNameConstant(tzinfoKeyword.value) and |
|
1999 AstUtilities.getValue(tzinfoKeyword.value) is None |
|
2000 ) |
|
2001 ) |
|
2002 |
|
2003 if not (isCase1 or isCase2): |
|
2004 self.violations.append((node, "M301")) |
|
2005 |
|
2006 elif node.func.attr == 'time': |
|
2007 # time(12, 10, 45, 0, datetime.timezone.utc) |
|
2008 isCase1 = ( |
|
2009 len(node.args) >= 5 and |
|
2010 not ( |
|
2011 AstUtilities.isNameConstant(node.args[4]) and |
|
2012 AstUtilities.getValue(node.args[4]) is None |
|
2013 ) |
|
2014 ) |
|
2015 |
|
2016 # datetime.time(12, 10, 45, tzinfo=datetime.timezone.utc) |
|
2017 tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo') |
|
2018 isCase2 = ( |
|
2019 tzinfoKeyword is not None and |
|
2020 not ( |
|
2021 AstUtilities.isNameConstant(tzinfoKeyword.value) and |
|
2022 AstUtilities.getValue(tzinfoKeyword.value) is None |
|
2023 ) |
|
2024 ) |
|
2025 |
|
2026 if not (isCase1 or isCase2): |
|
2027 self.violations.append((node, "M321")) |
|
2028 |
|
2029 elif node.func.attr == 'date': |
|
2030 self.violations.append((node, "M311")) |
|
2031 |
|
2032 if isDateTimeClass or isDateTimeModuleAndClass: |
|
2033 if node.func.attr == 'today': |
|
2034 self.violations.append((node, "M302")) |
|
2035 |
|
2036 elif node.func.attr == 'utcnow': |
|
2037 self.violations.append((node, "M303")) |
|
2038 |
|
2039 elif node.func.attr == 'utcfromtimestamp': |
|
2040 self.violations.append((node, "M304")) |
|
2041 |
|
2042 elif node.func.attr in 'now': |
|
2043 # datetime.now(UTC) |
|
2044 isCase1 = ( |
|
2045 len(node.args) == 1 and |
|
2046 len(node.keywords) == 0 and |
|
2047 not ( |
|
2048 AstUtilities.isNameConstant(node.args[0]) and |
|
2049 AstUtilities.getValue(node.args[0]) is None |
|
2050 ) |
|
2051 ) |
|
2052 |
|
2053 # datetime.now(tz=UTC) |
|
2054 tzKeyword = self.__getFromKeywords(node.keywords, 'tz') |
|
2055 isCase2 = ( |
|
2056 tzKeyword is not None and |
|
2057 not ( |
|
2058 AstUtilities.isNameConstant(tzKeyword.value) and |
|
2059 AstUtilities.getValue(tzKeyword.value) is None |
|
2060 ) |
|
2061 ) |
|
2062 |
|
2063 if not (isCase1 or isCase2): |
|
2064 self.violations.append((node, "M305")) |
|
2065 |
|
2066 elif node.func.attr == 'fromtimestamp': |
|
2067 # datetime.fromtimestamp(1234, UTC) |
|
2068 isCase1 = ( |
|
2069 len(node.args) == 2 and |
|
2070 len(node.keywords) == 0 and |
|
2071 not ( |
|
2072 AstUtilities.isNameConstant(node.args[1]) and |
|
2073 AstUtilities.getValue(node.args[1]) is None |
|
2074 ) |
|
2075 ) |
|
2076 |
|
2077 # datetime.fromtimestamp(1234, tz=UTC) |
|
2078 tzKeyword = self.__getFromKeywords(node.keywords, 'tz') |
|
2079 isCase2 = ( |
|
2080 tzKeyword is not None and |
|
2081 not ( |
|
2082 AstUtilities.isNameConstant(tzKeyword.value) and |
|
2083 AstUtilities.getValue(tzKeyword.value) is None |
|
2084 ) |
|
2085 ) |
|
2086 |
|
2087 if not (isCase1 or isCase2): |
|
2088 self.violations.append((node, "M306")) |
|
2089 |
|
2090 elif node.func.attr == 'strptime': |
|
2091 # datetime.strptime(...).replace(tzinfo=UTC) |
|
2092 parent = getattr(node, '_dtCheckerParent', None) |
|
2093 pparent = getattr(parent, '_dtCheckerParent', None) |
|
2094 if ( |
|
2095 not (isinstance(parent, ast.Attribute) and |
|
2096 parent.attr == 'replace') or |
|
2097 not isinstance(pparent, ast.Call) |
|
2098 ): |
|
2099 isCase1 = False |
|
2100 else: |
|
2101 tzinfoKeyword = self.__getFromKeywords(pparent.keywords, |
|
2102 'tzinfo') |
|
2103 isCase1 = ( |
|
2104 tzinfoKeyword is not None and |
|
2105 not ( |
|
2106 AstUtilities.isNameConstant( |
|
2107 tzinfoKeyword.value) and |
|
2108 AstUtilities.getValue(tzinfoKeyword.value) is None |
|
2109 ) |
|
2110 ) |
|
2111 |
|
2112 if not isCase1: |
|
2113 self.violations.append((node, "M307")) |
|
2114 |
|
2115 elif node.func.attr == 'fromordinal': |
|
2116 self.violations.append((node, "M308")) |
|
2117 |
|
2118 # date.something() |
|
2119 isDateClass = (isinstance(node.func, ast.Attribute) and |
|
2120 isinstance(node.func.value, ast.Name) and |
|
2121 node.func.value.id == 'date') |
|
2122 |
|
2123 # datetime.date.something() |
|
2124 isDateModuleAndClass = (isinstance(node.func, ast.Attribute) and |
|
2125 isinstance(node.func.value, ast.Attribute) and |
|
2126 node.func.value.attr == 'date' and |
|
2127 isinstance(node.func.value.value, ast.Name) and |
|
2128 node.func.value.value.id == 'datetime') |
|
2129 |
|
2130 if isDateClass or isDateModuleAndClass: |
|
2131 if node.func.attr == 'today': |
|
2132 self.violations.append((node, "M312")) |
|
2133 |
|
2134 elif node.func.attr == 'fromtimestamp': |
|
2135 self.violations.append((node, "M313")) |
|
2136 |
|
2137 elif node.func.attr == 'fromordinal': |
|
2138 self.violations.append((node, "M314")) |
|
2139 |
|
2140 elif node.func.attr == 'fromisoformat': |
|
2141 self.violations.append((node, "M315")) |
|
2142 |
|
2143 self.generic_visit(node) |
|
2144 |
|
2145 |
|
2146 class SysVersionVisitor(ast.NodeVisitor): |
|
2147 """ |
|
2148 Class implementing a node visitor to check the use of sys.version and |
|
2149 sys.version_info. |
|
2150 |
|
2151 Note: This class is modelled after flake8-2020 checker. |
|
2152 """ |
|
2153 def __init__(self): |
|
2154 """ |
|
2155 Constructor |
|
2156 """ |
|
2157 super().__init__() |
|
2158 |
|
2159 self.violations = [] |
|
2160 self.__fromImports = {} |
|
2161 |
|
2162 def visit_ImportFrom(self, node): |
|
2163 """ |
|
2164 Public method to handle a from ... import ... statement. |
|
2165 |
|
2166 @param node reference to the node to be processed |
|
2167 @type ast.ImportFrom |
|
2168 """ |
|
2169 for alias in node.names: |
|
2170 if node.module is not None and not alias.asname: |
|
2171 self.__fromImports[alias.name] = node.module |
|
2172 |
|
2173 self.generic_visit(node) |
|
2174 |
|
2175 def __isSys(self, attr, node): |
|
2176 """ |
|
2177 Private method to check for a reference to sys attribute. |
|
2178 |
|
2179 @param attr attribute name |
|
2180 @type str |
|
2181 @param node reference to the node to be checked |
|
2182 @type ast.Node |
|
2183 @return flag indicating a match |
|
2184 @rtype bool |
|
2185 """ |
|
2186 match = False |
|
2187 if ( |
|
2188 (isinstance(node, ast.Attribute) and |
|
2189 isinstance(node.value, ast.Name) and |
|
2190 node.value.id == "sys" and |
|
2191 node.attr == attr) or |
|
2192 (isinstance(node, ast.Name) and |
|
2193 node.id == attr and |
|
2194 self.__fromImports.get(node.id) == "sys") |
|
2195 ): |
|
2196 match = True |
|
2197 |
|
2198 return match |
|
2199 |
|
2200 def __isSysVersionUpperSlice(self, node, n): |
|
2201 """ |
|
2202 Private method to check the upper slice of sys.version. |
|
2203 |
|
2204 @param node reference to the node to be checked |
|
2205 @type ast.Node |
|
2206 @param n slice value to check against |
|
2207 @type int |
|
2208 @return flag indicating a match |
|
2209 @rtype bool |
|
2210 """ |
|
2211 return ( |
|
2212 self.__isSys("version", node.value) and |
|
2213 isinstance(node.slice, ast.Slice) and |
|
2214 node.slice.lower is None and |
|
2215 AstUtilities.isNumber(node.slice.upper) and |
|
2216 AstUtilities.getValue(node.slice.upper) == n and |
|
2217 node.slice.step is None |
|
2218 ) |
|
2219 |
|
2220 def visit_Subscript(self, node): |
|
2221 """ |
|
2222 Public method to handle a subscript. |
|
2223 |
|
2224 @param node reference to the node to be processed |
|
2225 @type ast.Subscript |
|
2226 """ |
|
2227 if self.__isSysVersionUpperSlice(node, 1): |
|
2228 self.violations.append((node.value, "M423")) |
|
2229 elif self.__isSysVersionUpperSlice(node, 3): |
|
2230 self.violations.append((node.value, "M401")) |
|
2231 elif ( |
|
2232 self.__isSys('version', node.value) and |
|
2233 isinstance(node.slice, ast.Index) and |
|
2234 AstUtilities.isNumber(node.slice.value) and |
|
2235 AstUtilities.getValue(node.slice.value) == 2 |
|
2236 ): |
|
2237 self.violations.append((node.value, "M402")) |
|
2238 elif ( |
|
2239 self.__isSys('version', node.value) and |
|
2240 isinstance(node.slice, ast.Index) and |
|
2241 AstUtilities.isNumber(node.slice.value) and |
|
2242 AstUtilities.getValue(node.slice.value) == 0 |
|
2243 ): |
|
2244 self.violations.append((node.value, "M421")) |
|
2245 |
|
2246 self.generic_visit(node) |
|
2247 |
|
2248 def visit_Compare(self, node): |
|
2249 """ |
|
2250 Public method to handle a comparison. |
|
2251 |
|
2252 @param node reference to the node to be processed |
|
2253 @type ast.Compare |
|
2254 """ |
|
2255 if ( |
|
2256 isinstance(node.left, ast.Subscript) and |
|
2257 self.__isSys('version_info', node.left.value) and |
|
2258 isinstance(node.left.slice, ast.Index) and |
|
2259 AstUtilities.isNumber(node.left.slice.value) and |
|
2260 AstUtilities.getValue(node.left.slice.value) == 0 and |
|
2261 len(node.ops) == 1 and |
|
2262 isinstance(node.ops[0], ast.Eq) and |
|
2263 AstUtilities.isNumber(node.comparators[0]) and |
|
2264 AstUtilities.getValue(node.comparators[0]) == 3 |
|
2265 ): |
|
2266 self.violations.append((node.left, "M411")) |
|
2267 elif ( |
|
2268 self.__isSys('version', node.left) and |
|
2269 len(node.ops) == 1 and |
|
2270 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and |
|
2271 AstUtilities.isString(node.comparators[0]) |
|
2272 ): |
|
2273 if len(AstUtilities.getValue(node.comparators[0])) == 1: |
|
2274 errorCode = "M422" |
|
2275 else: |
|
2276 errorCode = "M403" |
|
2277 self.violations.append((node.left, errorCode)) |
|
2278 elif ( |
|
2279 isinstance(node.left, ast.Subscript) and |
|
2280 self.__isSys('version_info', node.left.value) and |
|
2281 isinstance(node.left.slice, ast.Index) and |
|
2282 AstUtilities.isNumber(node.left.slice.value) and |
|
2283 AstUtilities.getValue(node.left.slice.value) == 1 and |
|
2284 len(node.ops) == 1 and |
|
2285 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and |
|
2286 AstUtilities.isNumber(node.comparators[0]) |
|
2287 ): |
|
2288 self.violations.append((node, "M413")) |
|
2289 elif ( |
|
2290 isinstance(node.left, ast.Attribute) and |
|
2291 self.__isSys('version_info', node.left.value) and |
|
2292 node.left.attr == 'minor' and |
|
2293 len(node.ops) == 1 and |
|
2294 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and |
|
2295 AstUtilities.isNumber(node.comparators[0]) |
|
2296 ): |
|
2297 self.violations.append((node, "M414")) |
|
2298 |
|
2299 self.generic_visit(node) |
|
2300 |
|
2301 def visit_Attribute(self, node): |
|
2302 """ |
|
2303 Public method to handle an attribute. |
|
2304 |
|
2305 @param node reference to the node to be processed |
|
2306 @type ast.Attribute |
|
2307 """ |
|
2308 if ( |
|
2309 isinstance(node.value, ast.Name) and |
|
2310 node.value.id == 'six' and |
|
2311 node.attr == 'PY3' |
|
2312 ): |
|
2313 self.violations.append((node, "M412")) |
|
2314 |
|
2315 self.generic_visit(node) |
|
2316 |
|
2317 def visit_Name(self, node): |
|
2318 """ |
|
2319 Public method to handle an name. |
|
2320 |
|
2321 @param node reference to the node to be processed |
|
2322 @type ast.Name |
|
2323 """ |
|
2324 if node.id == 'PY3' and self.__fromImports.get(node.id) == 'six': |
|
2325 self.violations.append((node, "M412")) |
|
2326 |
|
2327 self.generic_visit(node) |
|
2328 |
|
2329 # |
|
2330 # eflag: noqa = M891 |